@edgible-team/cli 1.0.1
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.
- package/LICENSE +136 -0
- package/README.md +450 -0
- package/dist/client/api-client.js +1057 -0
- package/dist/client/index.js +21 -0
- package/dist/commands/agent.js +1280 -0
- package/dist/commands/ai.js +608 -0
- package/dist/commands/application.js +885 -0
- package/dist/commands/auth.js +570 -0
- package/dist/commands/base/BaseCommand.js +93 -0
- package/dist/commands/base/CommandHandler.js +7 -0
- package/dist/commands/base/command-wrapper.js +58 -0
- package/dist/commands/base/middleware.js +77 -0
- package/dist/commands/config.js +116 -0
- package/dist/commands/connectivity.js +59 -0
- package/dist/commands/debug.js +98 -0
- package/dist/commands/discover.js +144 -0
- package/dist/commands/examples/migrated-command-example.js +180 -0
- package/dist/commands/gateway.js +494 -0
- package/dist/commands/managedGateway.js +787 -0
- package/dist/commands/utils/config-validator.js +76 -0
- package/dist/commands/utils/gateway-prompt.js +79 -0
- package/dist/commands/utils/input-parser.js +120 -0
- package/dist/commands/utils/output-formatter.js +109 -0
- package/dist/config/app-config.js +99 -0
- package/dist/detection/SystemCapabilityDetector.js +1244 -0
- package/dist/detection/ToolDetector.js +305 -0
- package/dist/detection/WorkloadDetector.js +314 -0
- package/dist/di/bindings.js +99 -0
- package/dist/di/container.js +88 -0
- package/dist/di/types.js +32 -0
- package/dist/index.js +52 -0
- package/dist/interfaces/IDaemonManager.js +3 -0
- package/dist/repositories/config-repository.js +62 -0
- package/dist/repositories/gateway-repository.js +35 -0
- package/dist/scripts/postinstall.js +101 -0
- package/dist/services/AgentStatusManager.js +299 -0
- package/dist/services/ConnectivityTester.js +271 -0
- package/dist/services/DependencyInstaller.js +475 -0
- package/dist/services/LocalAgentManager.js +2216 -0
- package/dist/services/application/ApplicationService.js +299 -0
- package/dist/services/auth/AuthService.js +214 -0
- package/dist/services/aws.js +644 -0
- package/dist/services/daemon/DaemonManagerFactory.js +65 -0
- package/dist/services/daemon/DockerDaemonManager.js +395 -0
- package/dist/services/daemon/LaunchdDaemonManager.js +257 -0
- package/dist/services/daemon/PodmanDaemonManager.js +369 -0
- package/dist/services/daemon/SystemdDaemonManager.js +221 -0
- package/dist/services/daemon/WindowsServiceDaemonManager.js +210 -0
- package/dist/services/daemon/index.js +16 -0
- package/dist/services/edgible.js +3060 -0
- package/dist/services/gateway/GatewayService.js +334 -0
- package/dist/state/config.js +146 -0
- package/dist/types/AgentConfig.js +5 -0
- package/dist/types/AgentStatus.js +5 -0
- package/dist/types/ApiClient.js +5 -0
- package/dist/types/ApiRequests.js +5 -0
- package/dist/types/ApiResponses.js +5 -0
- package/dist/types/Application.js +5 -0
- package/dist/types/CaddyJson.js +5 -0
- package/dist/types/UnifiedAgentStatus.js +56 -0
- package/dist/types/WireGuard.js +5 -0
- package/dist/types/Workload.js +5 -0
- package/dist/types/agent.js +5 -0
- package/dist/types/command-options.js +5 -0
- package/dist/types/connectivity.js +5 -0
- package/dist/types/errors.js +250 -0
- package/dist/types/gateway-types.js +5 -0
- package/dist/types/index.js +48 -0
- package/dist/types/models/ApplicationData.js +5 -0
- package/dist/types/models/CertificateData.js +5 -0
- package/dist/types/models/DeviceData.js +5 -0
- package/dist/types/models/DevicePoolData.js +5 -0
- package/dist/types/models/OrganizationData.js +5 -0
- package/dist/types/models/OrganizationInviteData.js +5 -0
- package/dist/types/models/ProviderConfiguration.js +5 -0
- package/dist/types/models/ResourceData.js +5 -0
- package/dist/types/models/ServiceResourceData.js +5 -0
- package/dist/types/models/UserData.js +5 -0
- package/dist/types/route.js +5 -0
- package/dist/types/validation/schemas.js +218 -0
- package/dist/types/validation.js +5 -0
- package/dist/utils/FileIntegrityManager.js +256 -0
- package/dist/utils/PathMigration.js +219 -0
- package/dist/utils/PathResolver.js +235 -0
- package/dist/utils/PlatformDetector.js +277 -0
- package/dist/utils/console-logger.js +130 -0
- package/dist/utils/docker-compose-parser.js +179 -0
- package/dist/utils/errors.js +130 -0
- package/dist/utils/health-checker.js +155 -0
- package/dist/utils/json-logger.js +72 -0
- package/dist/utils/log-formatter.js +293 -0
- package/dist/utils/logger.js +59 -0
- package/dist/utils/network-utils.js +217 -0
- package/dist/utils/output.js +182 -0
- package/dist/utils/passwordValidation.js +91 -0
- package/dist/utils/progress.js +167 -0
- package/dist/utils/sudo-checker.js +22 -0
- package/dist/utils/urls.js +32 -0
- package/dist/utils/validation.js +31 -0
- package/dist/validation/schemas.js +175 -0
- package/dist/validation/validator.js +67 -0
- package/package.json +83 -0
|
@@ -0,0 +1,3060 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.EdgibleService = void 0;
|
|
40
|
+
const client_1 = require("../client");
|
|
41
|
+
const passwordValidation_1 = require("../utils/passwordValidation");
|
|
42
|
+
const config_1 = require("../state/config");
|
|
43
|
+
const aws_1 = require("./aws");
|
|
44
|
+
const ConnectivityTester_1 = require("./ConnectivityTester");
|
|
45
|
+
const LocalAgentManager_1 = require("./LocalAgentManager");
|
|
46
|
+
const urls_1 = require("../utils/urls");
|
|
47
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const PathResolver_1 = require("../utils/PathResolver");
|
|
51
|
+
/**
|
|
52
|
+
* Edgible service interface
|
|
53
|
+
* Uses the real API client for authentication and device management
|
|
54
|
+
*/
|
|
55
|
+
class EdgibleService {
|
|
56
|
+
constructor(baseUrl) {
|
|
57
|
+
this.applications = [];
|
|
58
|
+
this.baseUrl = baseUrl || (0, urls_1.getApiBaseUrl)();
|
|
59
|
+
this.apiClient = (0, client_1.createApiClient)(this.baseUrl);
|
|
60
|
+
this.configManager = new config_1.ConfigManager();
|
|
61
|
+
// Restore tokens from config if available (synchronous)
|
|
62
|
+
this.restoreTokensFromConfigSync();
|
|
63
|
+
}
|
|
64
|
+
restoreTokensFromConfigSync() {
|
|
65
|
+
const config = this.configManager.getConfig();
|
|
66
|
+
if (config.accessToken) {
|
|
67
|
+
this.apiClient.setAccessToken(config.accessToken);
|
|
68
|
+
}
|
|
69
|
+
if (config.idToken) {
|
|
70
|
+
this.apiClient.setIdToken(config.idToken);
|
|
71
|
+
}
|
|
72
|
+
if (config.refreshToken) {
|
|
73
|
+
this.apiClient.setRefreshToken(config.refreshToken);
|
|
74
|
+
}
|
|
75
|
+
// Log token restoration status for debugging
|
|
76
|
+
console.log('[EdgibleService] Token restoration:', {
|
|
77
|
+
hasAccessToken: !!config.accessToken,
|
|
78
|
+
hasIdToken: !!config.idToken,
|
|
79
|
+
hasRefreshToken: !!config.refreshToken,
|
|
80
|
+
organizationId: config.organizationId,
|
|
81
|
+
deviceId: config.deviceId
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async attemptAutoRelogin() {
|
|
85
|
+
const config = this.configManager.getConfig();
|
|
86
|
+
// If no tokens but we have device credentials, attempt automatic re-login
|
|
87
|
+
if (!config.idToken && !config.accessToken && config.deviceId && config.devicePassword) {
|
|
88
|
+
console.log('[EdgibleService] No tokens available, attempting automatic device re-login...');
|
|
89
|
+
try {
|
|
90
|
+
await this.loginDevice(config.deviceId, config.devicePassword);
|
|
91
|
+
console.log('[EdgibleService] Automatic re-login successful');
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.log('[EdgibleService] Automatic re-login failed:', error instanceof Error ? error.message : String(error));
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
saveTokensToConfig() {
|
|
102
|
+
const accessToken = this.apiClient.getAccessToken();
|
|
103
|
+
const idToken = this.apiClient.getIdToken();
|
|
104
|
+
const refreshToken = this.apiClient.getRefreshToken();
|
|
105
|
+
console.log('[EdgibleService] Saving tokens:', {
|
|
106
|
+
hasAccessToken: !!accessToken,
|
|
107
|
+
hasIdToken: !!idToken,
|
|
108
|
+
hasRefreshToken: !!refreshToken
|
|
109
|
+
});
|
|
110
|
+
this.configManager.updateConfig({
|
|
111
|
+
accessToken,
|
|
112
|
+
idToken,
|
|
113
|
+
refreshToken
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Authenticate user with email and password
|
|
118
|
+
* @param email - Email address
|
|
119
|
+
* @param password - Password
|
|
120
|
+
* @returns Promise<LoginResponse> - Login response with tokens
|
|
121
|
+
*/
|
|
122
|
+
async loginUser(email, password) {
|
|
123
|
+
try {
|
|
124
|
+
const loginRequest = { email, password };
|
|
125
|
+
const response = await this.apiClient.login(loginRequest);
|
|
126
|
+
// The API client already sets the tokens internally, just save them to config
|
|
127
|
+
this.saveTokensToConfig();
|
|
128
|
+
return response;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error('Error logging in user:', error);
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Create a device with orphaned organization (device login without user account)
|
|
137
|
+
* @param deviceName - Name of the device
|
|
138
|
+
* @param password - Password for the device (must meet Cognito requirements)
|
|
139
|
+
* @param userEmail - Optional user email for invite
|
|
140
|
+
* @returns Promise<CreateDeviceWithOrphanedOrganizationResponse> - Device creation response
|
|
141
|
+
*/
|
|
142
|
+
async createDeviceWithOrganization(deviceName, password, userEmail) {
|
|
143
|
+
try {
|
|
144
|
+
// Validate password meets Cognito requirements
|
|
145
|
+
const passwordValidation = (0, passwordValidation_1.validateCognitoPassword)(password);
|
|
146
|
+
if (!passwordValidation.isValid) {
|
|
147
|
+
throw new Error(`Password validation failed: ${passwordValidation.errors.join(', ')}`);
|
|
148
|
+
}
|
|
149
|
+
const request = {
|
|
150
|
+
name: deviceName,
|
|
151
|
+
description: `CLI device: ${deviceName}`,
|
|
152
|
+
type: 'laptop', // Default type for CLI devices
|
|
153
|
+
organizationName: `${deviceName} Organization`,
|
|
154
|
+
organizationDescription: `Organization created for device ${deviceName}`,
|
|
155
|
+
userEmail: userEmail,
|
|
156
|
+
password: password
|
|
157
|
+
};
|
|
158
|
+
const response = await this.apiClient.createDeviceWithOrphanedOrganization(request);
|
|
159
|
+
// Note: The createDeviceWithOrphanedOrganization response doesn't include tokens
|
|
160
|
+
// Tokens would be obtained through a separate login call with the device credentials
|
|
161
|
+
return response;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
console.error('Error creating device with organization:', error);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if an account exists for the given email
|
|
170
|
+
* @param email - Email address to check
|
|
171
|
+
* @returns Promise<boolean> - True if account exists
|
|
172
|
+
*/
|
|
173
|
+
async checkAccount(email) {
|
|
174
|
+
try {
|
|
175
|
+
// Clear any existing tokens before checking account to ensure a clean login attempt
|
|
176
|
+
// This prevents interference from previous sessions
|
|
177
|
+
this.apiClient.clearTokens();
|
|
178
|
+
// Try to authenticate with a dummy password to check if account exists
|
|
179
|
+
// This is a simple way to check account existence without exposing real authentication
|
|
180
|
+
try {
|
|
181
|
+
await this.apiClient.login({ email, password: 'dummy' });
|
|
182
|
+
// Clear tokens again after check since we used a dummy password
|
|
183
|
+
this.apiClient.clearTokens();
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
// Clear tokens in case any were set during the failed attempt
|
|
188
|
+
this.apiClient.clearTokens();
|
|
189
|
+
// If error is "UserNotFoundException", account doesn't exist
|
|
190
|
+
if (error.message?.includes('UserNotFoundException') || error.message?.includes('NotAuthorizedException')) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
// For other errors, assume account exists but password is wrong
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
// Ensure tokens are cleared even on unexpected errors
|
|
199
|
+
this.apiClient.clearTokens();
|
|
200
|
+
console.error('Error checking account:', error);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Authenticate device with device ID and password
|
|
206
|
+
* This method is used when the CLI needs to authenticate as a device.
|
|
207
|
+
* NOTE: This will overwrite user tokens. Use verifyDeviceCredentials() for verification only.
|
|
208
|
+
* @param deviceId - Device ID
|
|
209
|
+
* @param password - Device password
|
|
210
|
+
* @returns Promise<LoginResponse> - Login response with tokens
|
|
211
|
+
*/
|
|
212
|
+
async loginDevice(deviceId, password) {
|
|
213
|
+
try {
|
|
214
|
+
const loginRequest = { email: deviceId, password };
|
|
215
|
+
const response = await this.apiClient.login(loginRequest);
|
|
216
|
+
// The API client already sets the tokens internally, just save them to config
|
|
217
|
+
// NOTE: This overwrites user tokens - use this only when CLI needs to act as device
|
|
218
|
+
this.saveTokensToConfig();
|
|
219
|
+
return response;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.error('Error logging in device:', error);
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Verify device credentials without overwriting user tokens
|
|
228
|
+
* This creates a temporary API client instance for verification
|
|
229
|
+
* @param deviceId - Device ID
|
|
230
|
+
* @param password - Device password
|
|
231
|
+
* @returns Promise<boolean> - True if credentials are valid
|
|
232
|
+
*/
|
|
233
|
+
async verifyDeviceCredentials(deviceId, password) {
|
|
234
|
+
try {
|
|
235
|
+
// Save current user tokens to restore later
|
|
236
|
+
const currentAccessToken = this.apiClient.getAccessToken();
|
|
237
|
+
const currentIdToken = this.apiClient.getIdToken();
|
|
238
|
+
const currentRefreshToken = this.apiClient.getRefreshToken();
|
|
239
|
+
// Create a temporary API client for verification
|
|
240
|
+
const tempApiClient = (0, client_1.createApiClient)(this.baseUrl);
|
|
241
|
+
const loginRequest = { email: deviceId, password };
|
|
242
|
+
try {
|
|
243
|
+
await tempApiClient.login(loginRequest);
|
|
244
|
+
// Verification successful
|
|
245
|
+
// Restore user tokens
|
|
246
|
+
if (currentAccessToken) {
|
|
247
|
+
this.apiClient.setAccessToken(currentAccessToken);
|
|
248
|
+
}
|
|
249
|
+
if (currentIdToken) {
|
|
250
|
+
this.apiClient.setIdToken(currentIdToken);
|
|
251
|
+
}
|
|
252
|
+
if (currentRefreshToken) {
|
|
253
|
+
this.apiClient.setRefreshToken(currentRefreshToken);
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
// Verification failed, but restore user tokens anyway
|
|
259
|
+
if (currentAccessToken) {
|
|
260
|
+
this.apiClient.setAccessToken(currentAccessToken);
|
|
261
|
+
}
|
|
262
|
+
if (currentIdToken) {
|
|
263
|
+
this.apiClient.setIdToken(currentIdToken);
|
|
264
|
+
}
|
|
265
|
+
if (currentRefreshToken) {
|
|
266
|
+
this.apiClient.setRefreshToken(currentRefreshToken);
|
|
267
|
+
}
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
console.error('Error verifying device credentials:', error);
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Refresh authentication tokens
|
|
278
|
+
* @param refreshToken - Refresh token
|
|
279
|
+
* @returns Promise<RefreshTokenResponse> - New tokens
|
|
280
|
+
*/
|
|
281
|
+
async refreshToken(refreshToken) {
|
|
282
|
+
try {
|
|
283
|
+
const response = await this.apiClient.refreshToken({ refreshToken });
|
|
284
|
+
// Save new tokens to config
|
|
285
|
+
this.saveTokensToConfig();
|
|
286
|
+
return response;
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
console.error('Error refreshing token:', error);
|
|
290
|
+
throw error;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get access token
|
|
295
|
+
* @returns string | undefined - Access token
|
|
296
|
+
*/
|
|
297
|
+
getAccessToken() {
|
|
298
|
+
return this.apiClient.getAccessToken();
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Get ID token
|
|
302
|
+
* @returns string | undefined - ID token
|
|
303
|
+
*/
|
|
304
|
+
getIdToken() {
|
|
305
|
+
return this.apiClient.getIdToken();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Get refresh token
|
|
309
|
+
* @returns string | undefined - Refresh token
|
|
310
|
+
*/
|
|
311
|
+
getRefreshToken() {
|
|
312
|
+
return this.apiClient.getRefreshToken();
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Create a new account
|
|
316
|
+
* @param email - Email address
|
|
317
|
+
* @param name - Full name
|
|
318
|
+
* @returns Promise<boolean> - True if account created successfully
|
|
319
|
+
*/
|
|
320
|
+
async createAccount(email, name) {
|
|
321
|
+
try {
|
|
322
|
+
// This would typically redirect to web signup or use a different endpoint
|
|
323
|
+
console.log(`Creating account for: ${email} (${name})`);
|
|
324
|
+
console.log('Account creation should be done through the web interface');
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
catch (error) {
|
|
328
|
+
console.error('Error creating account:', error);
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Get account status
|
|
334
|
+
* @param email - Email address
|
|
335
|
+
* @returns Promise<object> - Account status information
|
|
336
|
+
*/
|
|
337
|
+
async getAccountStatus(email) {
|
|
338
|
+
try {
|
|
339
|
+
// In a real implementation, this would make an API call
|
|
340
|
+
console.log(`Getting account status for: ${email}`);
|
|
341
|
+
// Simulate API call delay
|
|
342
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
343
|
+
// For demo purposes, return mock data
|
|
344
|
+
return {
|
|
345
|
+
email,
|
|
346
|
+
status: 'active',
|
|
347
|
+
createdAt: new Date().toISOString(),
|
|
348
|
+
lastLogin: new Date().toISOString()
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
console.error('Error getting account status:', error);
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Set up an application for a workload
|
|
358
|
+
* @param workload - The workload to set up an application for
|
|
359
|
+
* @param port - The port to expose
|
|
360
|
+
* @param protocol - The protocol to use
|
|
361
|
+
* @param description - Optional description
|
|
362
|
+
* @returns Promise<Application> - The created application
|
|
363
|
+
*/
|
|
364
|
+
async setupApplication(workload, port, protocol = 'http', description, gatewayId) {
|
|
365
|
+
try {
|
|
366
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
367
|
+
await this.attemptAutoRelogin();
|
|
368
|
+
console.log(`Setting up application for workload: ${workload.name}`);
|
|
369
|
+
// Get organization ID from config
|
|
370
|
+
const configManager = new config_1.ConfigManager();
|
|
371
|
+
const config = configManager.getConfig();
|
|
372
|
+
if (!config.organizationId) {
|
|
373
|
+
throw new Error('No organization ID found. Please login first.');
|
|
374
|
+
}
|
|
375
|
+
// Generate a serving IP (in real implementation, this would come from the API)
|
|
376
|
+
const servingIp = this.generateServingIp();
|
|
377
|
+
const url = `${protocol}://${servingIp}:${port}`;
|
|
378
|
+
// Create application via API
|
|
379
|
+
const deviceIds = [config.deviceId || 'unknown'];
|
|
380
|
+
const createRequest = {
|
|
381
|
+
name: `${workload.name}-app`,
|
|
382
|
+
description: description || `Application for ${workload.name}`,
|
|
383
|
+
organizationId: config.organizationId,
|
|
384
|
+
configuration: {
|
|
385
|
+
port: port,
|
|
386
|
+
protocol: protocol,
|
|
387
|
+
url
|
|
388
|
+
},
|
|
389
|
+
deviceIds: deviceIds,
|
|
390
|
+
gatewayIds: gatewayId ? [gatewayId] : [],
|
|
391
|
+
subtype: 'local-preexisting'
|
|
392
|
+
};
|
|
393
|
+
const response = await this.apiClient.createApplication(createRequest);
|
|
394
|
+
// Validate API response
|
|
395
|
+
if (!response || !response.application) {
|
|
396
|
+
throw new Error('Invalid API response: missing application data');
|
|
397
|
+
}
|
|
398
|
+
if (!response.application.id) {
|
|
399
|
+
throw new Error('Invalid API response: missing application ID');
|
|
400
|
+
}
|
|
401
|
+
// Map API response to CLI Application format
|
|
402
|
+
const application = {
|
|
403
|
+
id: response.application.id,
|
|
404
|
+
name: `${workload.name}-app`,
|
|
405
|
+
workloadId: workload.id,
|
|
406
|
+
servingIp: servingIp,
|
|
407
|
+
port: port,
|
|
408
|
+
protocol: protocol,
|
|
409
|
+
status: 'active',
|
|
410
|
+
createdAt: new Date().toISOString(),
|
|
411
|
+
description: description || `Application for ${workload.name}`
|
|
412
|
+
};
|
|
413
|
+
return application;
|
|
414
|
+
}
|
|
415
|
+
catch (error) {
|
|
416
|
+
console.error('Error setting up application:', error);
|
|
417
|
+
throw error;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get all applications
|
|
422
|
+
* @returns Promise<Application[]> - List of applications
|
|
423
|
+
*/
|
|
424
|
+
async getApplications() {
|
|
425
|
+
try {
|
|
426
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
427
|
+
await this.attemptAutoRelogin();
|
|
428
|
+
console.log('Getting applications...');
|
|
429
|
+
const configManager = new config_1.ConfigManager();
|
|
430
|
+
const config = configManager.getConfig();
|
|
431
|
+
let organizationId;
|
|
432
|
+
// Check if we have an organization ID in the config (device login)
|
|
433
|
+
if (config.organizationId) {
|
|
434
|
+
organizationId = config.organizationId;
|
|
435
|
+
}
|
|
436
|
+
else if (config.email) {
|
|
437
|
+
// User login - get their organizations first
|
|
438
|
+
console.log('Getting user organizations...');
|
|
439
|
+
const userOrgs = await this.apiClient.getUserOrganizations(config.email);
|
|
440
|
+
// Filter for standard (non-billing) organizations only
|
|
441
|
+
const standardOrgs = userOrgs.organizations.filter((org) => org.organizationType !== 'billing');
|
|
442
|
+
if (standardOrgs.length === 0) {
|
|
443
|
+
console.log('No standard organizations found for this user.');
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
// Use the first standard organization
|
|
447
|
+
organizationId = standardOrgs[0].organizationId;
|
|
448
|
+
console.log(`Using organization: ${standardOrgs[0].name}`);
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
console.log('No organization ID or email found. Please login first.');
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
// Call the real API to get organization applications
|
|
455
|
+
const response = await this.apiClient.getOrganizationApplications(organizationId);
|
|
456
|
+
// Map API response to CLI Application format
|
|
457
|
+
const applications = response.applications.map(app => ({
|
|
458
|
+
id: app.id,
|
|
459
|
+
name: app.name,
|
|
460
|
+
workloadId: app.deviceIds[0] || 'unknown', // Use first device ID as workload ID
|
|
461
|
+
servingIp: app.url ? new URL(app.url).hostname : 'unknown',
|
|
462
|
+
port: app.url ? parseInt(new URL(app.url).port) || 80 : 80,
|
|
463
|
+
protocol: app.url && app.url.startsWith('https') ? 'https' : 'http',
|
|
464
|
+
status: app.status === 'running' ? 'active' : 'inactive',
|
|
465
|
+
createdAt: app.createdAt,
|
|
466
|
+
description: app.description
|
|
467
|
+
}));
|
|
468
|
+
return applications;
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
console.error('Error getting applications:', error);
|
|
472
|
+
throw error;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Get application by ID
|
|
477
|
+
* @param id - Application ID
|
|
478
|
+
* @returns Promise<Application | null> - The application or null if not found
|
|
479
|
+
*/
|
|
480
|
+
async getApplication(id) {
|
|
481
|
+
try {
|
|
482
|
+
// Try to get from API first
|
|
483
|
+
try {
|
|
484
|
+
const response = await this.apiClient.getApplication(id);
|
|
485
|
+
const appData = response.application;
|
|
486
|
+
// Extract port and protocol from configuration if needed
|
|
487
|
+
const port = appData.configuration?.port || 3000;
|
|
488
|
+
const protocol = (appData.configuration?.protocol || 'http');
|
|
489
|
+
const workloadId = appData.configuration?.workloadId || '';
|
|
490
|
+
return {
|
|
491
|
+
id: appData.id,
|
|
492
|
+
name: appData.name,
|
|
493
|
+
description: appData.description || '',
|
|
494
|
+
servingIp: this.generateServingIp(),
|
|
495
|
+
port: typeof port === 'number' ? port : 3000,
|
|
496
|
+
protocol: protocol,
|
|
497
|
+
status: appData.status === 'running' ? 'active' : 'inactive',
|
|
498
|
+
workloadId: workloadId || undefined,
|
|
499
|
+
createdAt: appData.createdAt || new Date().toISOString(),
|
|
500
|
+
url: appData.url
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
catch (apiError) {
|
|
504
|
+
// Fall back to local storage if API fails
|
|
505
|
+
const application = this.applications.find(app => app.id === id);
|
|
506
|
+
if (application) {
|
|
507
|
+
return application;
|
|
508
|
+
}
|
|
509
|
+
throw new Error(`Application not found: ${id}`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
console.error('Error getting application:', error);
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Delete an application
|
|
519
|
+
* @param id - Application ID
|
|
520
|
+
* @returns Promise<boolean> - True if deleted successfully
|
|
521
|
+
*/
|
|
522
|
+
async deleteApplication(id) {
|
|
523
|
+
try {
|
|
524
|
+
console.log(`Deleting application: ${id}`);
|
|
525
|
+
// Simulate API call delay
|
|
526
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
527
|
+
const index = this.applications.findIndex(app => app.id === id);
|
|
528
|
+
if (index !== -1) {
|
|
529
|
+
this.applications.splice(index, 1);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
console.error('Error deleting application:', error);
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Generate a mock serving IP address
|
|
541
|
+
* In a real implementation, this would be provided by the Edgible service
|
|
542
|
+
*/
|
|
543
|
+
generateServingIp() {
|
|
544
|
+
// Generate a mock IP in the 10.x.x.x range
|
|
545
|
+
const octet1 = 10;
|
|
546
|
+
const octet2 = Math.floor(Math.random() * 255);
|
|
547
|
+
const octet3 = Math.floor(Math.random() * 255);
|
|
548
|
+
const octet4 = Math.floor(Math.random() * 254) + 1; // Avoid .0
|
|
549
|
+
return `${octet1}.${octet2}.${octet3}.${octet4}`;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Get user organizations
|
|
553
|
+
* @param email - Email address
|
|
554
|
+
* @returns Promise<GetUserOrganizationsResponse> - User organizations
|
|
555
|
+
*/
|
|
556
|
+
async getUserOrganizations(email) {
|
|
557
|
+
try {
|
|
558
|
+
return await this.apiClient.getUserOrganizations(email);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
console.error('Error getting user organizations:', error);
|
|
562
|
+
throw error;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Gateway Management Methods
|
|
566
|
+
/**
|
|
567
|
+
* Create a gateway device and EC2 instance
|
|
568
|
+
*/
|
|
569
|
+
async createGateway(config) {
|
|
570
|
+
try {
|
|
571
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
572
|
+
await this.attemptAutoRelogin();
|
|
573
|
+
console.log(chalk_1.default.blue('Creating gateway...'));
|
|
574
|
+
// Get organization ID
|
|
575
|
+
const configManager = new config_1.ConfigManager();
|
|
576
|
+
const userConfig = configManager.getConfig();
|
|
577
|
+
if (!userConfig.organizationId) {
|
|
578
|
+
throw new Error('No organization ID found. Please login first.');
|
|
579
|
+
}
|
|
580
|
+
// Initialize AWS service
|
|
581
|
+
const awsService = new aws_1.AWSService(config.awsProfile, config.region);
|
|
582
|
+
// Check AWS CLI availability
|
|
583
|
+
const awsCheck = await awsService.checkAWSCLI();
|
|
584
|
+
if (!awsCheck.available) {
|
|
585
|
+
throw new Error('AWS CLI is not installed or not available');
|
|
586
|
+
}
|
|
587
|
+
// Validate AWS credentials
|
|
588
|
+
const credentialsValid = await awsService.validateCredentials(config.awsProfile);
|
|
589
|
+
if (!credentialsValid) {
|
|
590
|
+
throw new Error('AWS credentials are invalid or not configured');
|
|
591
|
+
}
|
|
592
|
+
// Generate unique key pair name
|
|
593
|
+
const keyPairName = `edgible-gateway-${Date.now()}`;
|
|
594
|
+
// Create key pair
|
|
595
|
+
console.log(chalk_1.default.gray('Creating SSH key pair...'));
|
|
596
|
+
const keyPair = await awsService.createKeyPair(keyPairName);
|
|
597
|
+
// Save SSH key locally
|
|
598
|
+
const keyPath = awsService.saveSSHKey(keyPair, keyPairName);
|
|
599
|
+
console.log(chalk_1.default.green(`✓ SSH key saved to: ${keyPath}`));
|
|
600
|
+
// Create EC2 instance
|
|
601
|
+
console.log(chalk_1.default.gray('Creating EC2 instance...'));
|
|
602
|
+
const ec2Instance = await awsService.createEC2Instance({
|
|
603
|
+
name: config.name,
|
|
604
|
+
instanceType: config.instanceType || 't3.micro',
|
|
605
|
+
keyPairName: keyPairName,
|
|
606
|
+
userData: this.generateAgentUserData(userConfig.deviceId || 'unknown')
|
|
607
|
+
});
|
|
608
|
+
console.log(chalk_1.default.green(`✓ EC2 instance created: ${ec2Instance.instanceId}`));
|
|
609
|
+
// Create gateway device via API (regular device with gateway type)
|
|
610
|
+
console.log(chalk_1.default.gray('Creating gateway device...'));
|
|
611
|
+
const deviceRequest = {
|
|
612
|
+
name: config.name,
|
|
613
|
+
description: config.description || `Gateway: ${config.name}`,
|
|
614
|
+
organizationId: userConfig.organizationId,
|
|
615
|
+
type: 'gateway',
|
|
616
|
+
userEmail: userConfig.email || '',
|
|
617
|
+
configuration: {
|
|
618
|
+
awsProfile: config.awsProfile,
|
|
619
|
+
region: config.region || 'us-east-1',
|
|
620
|
+
instanceType: config.instanceType || 't3.micro',
|
|
621
|
+
keyPairName: keyPairName,
|
|
622
|
+
ec2InstanceId: ec2Instance.instanceId,
|
|
623
|
+
publicIp: ec2Instance.publicIp,
|
|
624
|
+
privateIp: ec2Instance.privateIp,
|
|
625
|
+
state: ec2Instance.state
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
const response = await this.apiClient.createDevice(deviceRequest);
|
|
629
|
+
// Store gateway info in config
|
|
630
|
+
configManager.addGateway(response.device.id, {
|
|
631
|
+
name: config.name,
|
|
632
|
+
deviceId: response.device.id,
|
|
633
|
+
ec2InstanceId: ec2Instance.instanceId,
|
|
634
|
+
publicIp: ec2Instance.publicIp,
|
|
635
|
+
privateIp: ec2Instance.privateIp,
|
|
636
|
+
keyPairName: keyPairName,
|
|
637
|
+
keyPath: keyPath,
|
|
638
|
+
region: config.region || 'us-east-1'
|
|
639
|
+
});
|
|
640
|
+
// Store AWS profile and region
|
|
641
|
+
if (config.awsProfile) {
|
|
642
|
+
configManager.setAWSProfile(config.awsProfile);
|
|
643
|
+
}
|
|
644
|
+
if (config.region) {
|
|
645
|
+
configManager.setAWSRegion(config.region);
|
|
646
|
+
}
|
|
647
|
+
console.log(chalk_1.default.green('✓ Gateway created successfully!'));
|
|
648
|
+
return {
|
|
649
|
+
message: 'Gateway created successfully',
|
|
650
|
+
gateway: {
|
|
651
|
+
id: response.device.id,
|
|
652
|
+
name: response.device.name,
|
|
653
|
+
description: response.device.description,
|
|
654
|
+
organizationId: response.device.organizationId,
|
|
655
|
+
createdAt: response.device.createdAt
|
|
656
|
+
},
|
|
657
|
+
ec2Instance: {
|
|
658
|
+
instanceId: ec2Instance.instanceId,
|
|
659
|
+
publicIp: ec2Instance.publicIp,
|
|
660
|
+
privateIp: ec2Instance.privateIp,
|
|
661
|
+
state: ec2Instance.state,
|
|
662
|
+
keyPairName: keyPairName,
|
|
663
|
+
region: config.region || 'us-east-1'
|
|
664
|
+
},
|
|
665
|
+
sshKey: {
|
|
666
|
+
privateKey: keyPair.privateKey,
|
|
667
|
+
publicKey: keyPair.publicKey,
|
|
668
|
+
keyName: keyPairName
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
console.error(chalk_1.default.red('Error creating gateway:'), error);
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/**
|
|
678
|
+
* Create a serving device in the organization
|
|
679
|
+
*/
|
|
680
|
+
async createServingDevice(config) {
|
|
681
|
+
try {
|
|
682
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
683
|
+
await this.attemptAutoRelogin();
|
|
684
|
+
const userConfig = this.configManager.getConfig();
|
|
685
|
+
if (!userConfig.organizationId) {
|
|
686
|
+
throw new Error('No organization ID found. Please login first.');
|
|
687
|
+
}
|
|
688
|
+
// Create device via API - password will be generated by the API
|
|
689
|
+
const deviceRequest = {
|
|
690
|
+
name: config.name,
|
|
691
|
+
description: config.description || `Serving device: ${config.name}`,
|
|
692
|
+
organizationId: userConfig.organizationId,
|
|
693
|
+
type: 'server',
|
|
694
|
+
userEmail: userConfig.email || ''
|
|
695
|
+
};
|
|
696
|
+
const response = await this.apiClient.createDevice(deviceRequest);
|
|
697
|
+
return {
|
|
698
|
+
device: {
|
|
699
|
+
id: response.device.id,
|
|
700
|
+
name: response.device.name,
|
|
701
|
+
description: response.device.description,
|
|
702
|
+
type: response.device.type,
|
|
703
|
+
organizationId: response.device.organizationId,
|
|
704
|
+
createdAt: response.device.createdAt,
|
|
705
|
+
password: response.device.password // Include password from API response
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
catch (error) {
|
|
710
|
+
console.error(chalk_1.default.red('Error creating serving device:'), error);
|
|
711
|
+
throw error;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* List all serving devices
|
|
716
|
+
*/
|
|
717
|
+
async listServingDevices() {
|
|
718
|
+
try {
|
|
719
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
720
|
+
await this.attemptAutoRelogin();
|
|
721
|
+
const configManager = new config_1.ConfigManager();
|
|
722
|
+
const userConfig = configManager.getConfig();
|
|
723
|
+
if (!userConfig.organizationId) {
|
|
724
|
+
throw new Error('No organization ID found. Please login first.');
|
|
725
|
+
}
|
|
726
|
+
// Get all devices from the organization
|
|
727
|
+
const devicesResponse = await this.apiClient.getOrganizationDevices(userConfig.organizationId);
|
|
728
|
+
// Filter for serving devices (devices with type 'server')
|
|
729
|
+
const servingDevices = devicesResponse.devices.filter((device) => device.type === 'server');
|
|
730
|
+
// Map serving devices
|
|
731
|
+
const devices = servingDevices.map((device) => ({
|
|
732
|
+
id: device.id,
|
|
733
|
+
name: device.name,
|
|
734
|
+
description: device.description,
|
|
735
|
+
type: device.type,
|
|
736
|
+
organizationId: device.organizationId,
|
|
737
|
+
createdAt: device.createdAt
|
|
738
|
+
}));
|
|
739
|
+
return {
|
|
740
|
+
devices
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
console.error(chalk_1.default.red('Error listing serving devices:'), error);
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Get a specific device by ID
|
|
750
|
+
*/
|
|
751
|
+
async getDevice(deviceId) {
|
|
752
|
+
try {
|
|
753
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
754
|
+
await this.attemptAutoRelogin();
|
|
755
|
+
// Call the API to get device details
|
|
756
|
+
const deviceResponse = await this.apiClient.getDevice(deviceId);
|
|
757
|
+
return deviceResponse;
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
console.error(chalk_1.default.red('Error fetching device:'), error);
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* List all gateways
|
|
766
|
+
*/
|
|
767
|
+
async listGateways() {
|
|
768
|
+
try {
|
|
769
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
770
|
+
await this.attemptAutoRelogin();
|
|
771
|
+
const configManager = new config_1.ConfigManager();
|
|
772
|
+
const userConfig = configManager.getConfig();
|
|
773
|
+
if (!userConfig.organizationId) {
|
|
774
|
+
throw new Error('No organization ID found. Please login first.');
|
|
775
|
+
}
|
|
776
|
+
// Get all devices from the organization
|
|
777
|
+
const devicesResponse = await this.apiClient.getOrganizationDevices(userConfig.organizationId);
|
|
778
|
+
// Filter for gateway devices
|
|
779
|
+
const gatewayDevices = devicesResponse.devices.filter((device) => device.type === 'gateway');
|
|
780
|
+
// Enrich with local gateway info
|
|
781
|
+
const gateways = gatewayDevices.map((device) => {
|
|
782
|
+
const localInfo = configManager.getGateway(device.id);
|
|
783
|
+
return {
|
|
784
|
+
device: {
|
|
785
|
+
id: device.id,
|
|
786
|
+
name: device.name,
|
|
787
|
+
description: device.description,
|
|
788
|
+
type: device.type,
|
|
789
|
+
organizationId: device.organizationId,
|
|
790
|
+
createdAt: device.createdAt
|
|
791
|
+
},
|
|
792
|
+
ec2Instance: localInfo ? {
|
|
793
|
+
instanceId: localInfo.ec2InstanceId,
|
|
794
|
+
publicIp: localInfo.publicIp,
|
|
795
|
+
privateIp: localInfo.privateIp,
|
|
796
|
+
state: 'running', // Assume running if in config
|
|
797
|
+
region: localInfo.region
|
|
798
|
+
} : undefined,
|
|
799
|
+
applicationsCount: 0 // TODO: Calculate from local applications
|
|
800
|
+
};
|
|
801
|
+
});
|
|
802
|
+
return {
|
|
803
|
+
gateways
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
catch (error) {
|
|
807
|
+
console.error(chalk_1.default.red('Error listing gateways:'), error);
|
|
808
|
+
throw error;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Delete a gateway
|
|
813
|
+
*/
|
|
814
|
+
async deleteGateway(gatewayId, force = false) {
|
|
815
|
+
try {
|
|
816
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
817
|
+
await this.attemptAutoRelogin();
|
|
818
|
+
const configManager = new config_1.ConfigManager();
|
|
819
|
+
const gatewayInfo = configManager.getGateway(gatewayId);
|
|
820
|
+
if (!gatewayInfo) {
|
|
821
|
+
throw new Error('Gateway not found in local configuration');
|
|
822
|
+
}
|
|
823
|
+
// Check if gateway has active applications (unless forced)
|
|
824
|
+
if (!force) {
|
|
825
|
+
const applications = await this.getGatewayApplications(gatewayId);
|
|
826
|
+
if (applications.length > 0) {
|
|
827
|
+
throw new Error(`Gateway has ${applications.length} active applications. Use --force to delete anyway.`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// Terminate EC2 instance
|
|
831
|
+
const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
|
|
832
|
+
await awsService.terminateInstance(gatewayInfo.ec2InstanceId);
|
|
833
|
+
console.log(chalk_1.default.green(`✓ EC2 instance terminated: ${gatewayInfo.ec2InstanceId}`));
|
|
834
|
+
// Delete key pair
|
|
835
|
+
await awsService.deleteKeyPair(gatewayInfo.keyPairName);
|
|
836
|
+
console.log(chalk_1.default.green(`✓ Key pair deleted: ${gatewayInfo.keyPairName}`));
|
|
837
|
+
// Delete gateway device via API
|
|
838
|
+
const response = await this.apiClient.deleteDevice(gatewayId);
|
|
839
|
+
// Remove from local config
|
|
840
|
+
configManager.removeGateway(gatewayId);
|
|
841
|
+
console.log(chalk_1.default.green('✓ Gateway deleted successfully!'));
|
|
842
|
+
return {
|
|
843
|
+
success: true,
|
|
844
|
+
message: 'Gateway deleted successfully'
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
console.error(chalk_1.default.red('Error deleting gateway:'), error);
|
|
849
|
+
throw error;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Resync agent on gateway
|
|
854
|
+
*/
|
|
855
|
+
// Managed Gateway Methods (admin/debug only)
|
|
856
|
+
/**
|
|
857
|
+
* Create a managed gateway (admin only)
|
|
858
|
+
*/
|
|
859
|
+
async createManagedGateway(config) {
|
|
860
|
+
try {
|
|
861
|
+
await this.attemptAutoRelogin();
|
|
862
|
+
console.log(chalk_1.default.blue('Creating managed gateway...'));
|
|
863
|
+
const response = await this.apiClient.createManagedGateway({
|
|
864
|
+
region: config.region || 'us-east-1',
|
|
865
|
+
instanceType: config.instanceType || 't3.micro',
|
|
866
|
+
name: config.name
|
|
867
|
+
});
|
|
868
|
+
console.log(chalk_1.default.green('✓ Managed gateway created successfully!'));
|
|
869
|
+
return response;
|
|
870
|
+
}
|
|
871
|
+
catch (error) {
|
|
872
|
+
console.error(chalk_1.default.red('Error creating managed gateway:'), error);
|
|
873
|
+
throw error;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* List all managed gateways (admin only)
|
|
878
|
+
*/
|
|
879
|
+
async listManagedGateways() {
|
|
880
|
+
try {
|
|
881
|
+
await this.attemptAutoRelogin();
|
|
882
|
+
const response = await this.apiClient.listManagedGateways();
|
|
883
|
+
return response;
|
|
884
|
+
}
|
|
885
|
+
catch (error) {
|
|
886
|
+
console.error(chalk_1.default.red('Error listing managed gateways:'), error);
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Get managed gateway details (admin only)
|
|
892
|
+
*/
|
|
893
|
+
async getManagedGateway(gatewayId) {
|
|
894
|
+
try {
|
|
895
|
+
await this.attemptAutoRelogin();
|
|
896
|
+
const response = await this.apiClient.getManagedGateway({ gatewayId });
|
|
897
|
+
return response;
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
console.error(chalk_1.default.red('Error getting managed gateway:'), error);
|
|
901
|
+
throw error;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Delete a managed gateway (admin only)
|
|
906
|
+
*/
|
|
907
|
+
async deleteManagedGateway(gatewayId, force = false) {
|
|
908
|
+
try {
|
|
909
|
+
await this.attemptAutoRelogin();
|
|
910
|
+
await this.apiClient.deleteManagedGateway({
|
|
911
|
+
gatewayId,
|
|
912
|
+
force
|
|
913
|
+
});
|
|
914
|
+
console.log(chalk_1.default.green('✓ Managed gateway deleted successfully!'));
|
|
915
|
+
return { success: true };
|
|
916
|
+
}
|
|
917
|
+
catch (error) {
|
|
918
|
+
console.error(chalk_1.default.red('Error deleting managed gateway:'), error);
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Resync agent on a managed gateway (admin only)
|
|
924
|
+
*/
|
|
925
|
+
async resyncManagedGatewayAgent(gatewayId, agentVersion, installFromLocal, reboot, wipeLogs, debug) {
|
|
926
|
+
try {
|
|
927
|
+
await this.attemptAutoRelogin();
|
|
928
|
+
// Get managed gateway details to get SSH info
|
|
929
|
+
const gatewayDetails = await this.getManagedGateway(gatewayId);
|
|
930
|
+
if (!gatewayDetails.success || !gatewayDetails.gateway) {
|
|
931
|
+
throw new Error('Managed gateway not found');
|
|
932
|
+
}
|
|
933
|
+
const gateway = gatewayDetails.gateway;
|
|
934
|
+
const ipAddress = gateway.ipAddress;
|
|
935
|
+
if (!ipAddress) {
|
|
936
|
+
throw new Error('Gateway IP address not available');
|
|
937
|
+
}
|
|
938
|
+
// Get SSH key from backend
|
|
939
|
+
console.log(chalk_1.default.gray('Fetching SSH key from backend...'));
|
|
940
|
+
const sshKeyResponse = await this.apiClient.getManagedGatewaySSHKey({ gatewayId });
|
|
941
|
+
if (!sshKeyResponse.success || !sshKeyResponse.sshPrivateKey) {
|
|
942
|
+
throw new Error('Failed to retrieve SSH key for managed gateway');
|
|
943
|
+
}
|
|
944
|
+
const sshPrivateKey = sshKeyResponse.sshPrivateKey;
|
|
945
|
+
// Use AWSService to handle SSH operations
|
|
946
|
+
const { AWSService } = await Promise.resolve().then(() => __importStar(require('./aws')));
|
|
947
|
+
const awsService = new AWSService();
|
|
948
|
+
// Create temporary SSH key file
|
|
949
|
+
const os = require('os');
|
|
950
|
+
const path = require('path');
|
|
951
|
+
const fs = require('fs');
|
|
952
|
+
const tempKeyPath = path.join(os.tmpdir(), `edgible-mgw-${gatewayId}-${Date.now()}.pem`);
|
|
953
|
+
try {
|
|
954
|
+
// Write SSH key to temp file
|
|
955
|
+
fs.writeFileSync(tempKeyPath, sshPrivateKey, { mode: 0o600 });
|
|
956
|
+
// Create SSH connection
|
|
957
|
+
const connection = {
|
|
958
|
+
host: ipAddress,
|
|
959
|
+
port: 22,
|
|
960
|
+
username: 'ec2-user',
|
|
961
|
+
privateKey: sshPrivateKey
|
|
962
|
+
};
|
|
963
|
+
// Wait for instance to be ready for SSH
|
|
964
|
+
console.log(chalk_1.default.gray('Waiting for instance to be ready for SSH...'));
|
|
965
|
+
// Note: For managed gateways, we may not have the EC2 instance ID readily available
|
|
966
|
+
// We'll try to connect directly
|
|
967
|
+
await new Promise(resolve => setTimeout(resolve, 2000)); // Brief wait
|
|
968
|
+
// Test SSH connection
|
|
969
|
+
console.log(chalk_1.default.gray('Testing SSH connection...'));
|
|
970
|
+
try {
|
|
971
|
+
await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
|
|
972
|
+
console.log(chalk_1.default.green('✓ SSH connection established'));
|
|
973
|
+
}
|
|
974
|
+
catch (sshError) {
|
|
975
|
+
console.error(chalk_1.default.red('✗ SSH connection failed:'), sshError);
|
|
976
|
+
throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
|
|
977
|
+
}
|
|
978
|
+
// Wipe logs if requested (before stopping service)
|
|
979
|
+
let logsWiped = false;
|
|
980
|
+
if (wipeLogs) {
|
|
981
|
+
console.log(chalk_1.default.gray('Wiping agent logs...'));
|
|
982
|
+
try {
|
|
983
|
+
const wipeResult = await this.wipeManagedGatewayLogs(gatewayId);
|
|
984
|
+
if (wipeResult.success) {
|
|
985
|
+
console.log(chalk_1.default.green('✓ Logs wiped successfully'));
|
|
986
|
+
logsWiped = true;
|
|
987
|
+
}
|
|
988
|
+
else {
|
|
989
|
+
console.log(chalk_1.default.yellow('⚠ Failed to wipe logs, continuing with resync'));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
catch (wipeError) {
|
|
993
|
+
console.log(chalk_1.default.yellow(`⚠ Error wiping logs: ${wipeError instanceof Error ? wipeError.message : String(wipeError)}, continuing with resync`));
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
// Use the same resync logic as regular gateways
|
|
997
|
+
// Stop agent service
|
|
998
|
+
console.log(chalk_1.default.gray('Stopping agent service...'));
|
|
999
|
+
await awsService.executeSSHCommand(connection, 'sudo systemctl stop edgible-agent');
|
|
1000
|
+
// Ensure the agent directory exists
|
|
1001
|
+
console.log(chalk_1.default.gray('Creating agent directory...'));
|
|
1002
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /opt/edgible-agent');
|
|
1003
|
+
await awsService.executeSSHCommand(connection, 'sudo chown root:root /opt/edgible-agent');
|
|
1004
|
+
// Check Node.js installation and path
|
|
1005
|
+
console.log(chalk_1.default.gray('Checking Node.js installation...'));
|
|
1006
|
+
const nodeCheck = await awsService.executeSSHCommand(connection, 'which node || echo "node not found"');
|
|
1007
|
+
const nodePath = nodeCheck.stdout.trim();
|
|
1008
|
+
console.log(chalk_1.default.gray(`Node.js path: ${nodePath}`));
|
|
1009
|
+
if (nodePath === 'node not found') {
|
|
1010
|
+
console.log(chalk_1.default.gray('Installing Node.js...'));
|
|
1011
|
+
await awsService.executeSSHCommand(connection, 'curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -');
|
|
1012
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y nodejs');
|
|
1013
|
+
}
|
|
1014
|
+
// Check and install required system dependencies
|
|
1015
|
+
console.log(chalk_1.default.gray('Checking system dependencies...'));
|
|
1016
|
+
// Check iptables
|
|
1017
|
+
const iptablesCheck = await awsService.executeSSHCommand(connection, 'which iptables || echo "iptables not found"');
|
|
1018
|
+
if (iptablesCheck.stdout.trim() === 'iptables not found') {
|
|
1019
|
+
console.log(chalk_1.default.gray('Installing iptables...'));
|
|
1020
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y iptables');
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
console.log(chalk_1.default.gray('✓ iptables is installed'));
|
|
1024
|
+
}
|
|
1025
|
+
// Check WireGuard
|
|
1026
|
+
const wireguardCheck = await awsService.executeSSHCommand(connection, 'which wg || echo "wireguard not found"');
|
|
1027
|
+
if (wireguardCheck.stdout.trim() === 'wireguard not found') {
|
|
1028
|
+
console.log(chalk_1.default.gray('Installing WireGuard...'));
|
|
1029
|
+
await awsService.executeSSHCommand(connection, 'sudo amazon-linux-extras install epel -y || sudo yum install -y epel-release');
|
|
1030
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y wireguard-tools');
|
|
1031
|
+
await awsService.executeSSHCommand(connection, 'sudo modprobe wireguard || true');
|
|
1032
|
+
}
|
|
1033
|
+
else {
|
|
1034
|
+
console.log(chalk_1.default.gray('✓ WireGuard is installed'));
|
|
1035
|
+
}
|
|
1036
|
+
// Check HAProxy (gateway devices use HAProxy instead of Caddy)
|
|
1037
|
+
const haproxyCheck = await awsService.executeSSHCommand(connection, 'which haproxy 2>/dev/null || test -f /usr/sbin/haproxy && echo "haproxy found" || echo "haproxy not found"');
|
|
1038
|
+
if (haproxyCheck.stdout.trim() === 'haproxy not found' || (haproxyCheck.stdout.trim() === '' && !haproxyCheck.stdout.includes('haproxy found'))) {
|
|
1039
|
+
console.log(chalk_1.default.gray('Installing HAProxy...'));
|
|
1040
|
+
// Install HAProxy via package manager
|
|
1041
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y haproxy');
|
|
1042
|
+
// Create HAProxy config directory
|
|
1043
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /etc/haproxy');
|
|
1044
|
+
// Create HAProxy runtime socket directory
|
|
1045
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /var/run/haproxy');
|
|
1046
|
+
// Set capabilities for HAProxy to bind to privileged ports
|
|
1047
|
+
await awsService.executeSSHCommand(connection, 'sudo setcap cap_net_bind_service=+ep /usr/sbin/haproxy || true');
|
|
1048
|
+
// Create HAProxy chroot directory
|
|
1049
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /var/lib/haproxy');
|
|
1050
|
+
await awsService.executeSSHCommand(connection, 'sudo chown haproxy:haproxy /var/lib/haproxy || sudo chown root:root /var/lib/haproxy || true');
|
|
1051
|
+
// Verify installation
|
|
1052
|
+
const verifyResult = await awsService.executeSSHCommand(connection, '/usr/sbin/haproxy -v 2>&1');
|
|
1053
|
+
if (!verifyResult.stdout.includes('HAProxy') && !verifyResult.stdout.includes('version')) {
|
|
1054
|
+
throw new Error(`HAProxy installation verification failed. Output: ${verifyResult.stdout}`);
|
|
1055
|
+
}
|
|
1056
|
+
console.log(chalk_1.default.green('✓ HAProxy installed successfully'));
|
|
1057
|
+
}
|
|
1058
|
+
else {
|
|
1059
|
+
console.log(chalk_1.default.gray('✓ HAProxy is installed'));
|
|
1060
|
+
// Verify it's accessible
|
|
1061
|
+
const verifyResult = await awsService.executeSSHCommand(connection, 'haproxy -v 2>&1 || /usr/sbin/haproxy -v 2>&1');
|
|
1062
|
+
if (verifyResult.stdout.includes('command not found') && !verifyResult.stdout.includes('HAProxy') && !verifyResult.stdout.includes('version')) {
|
|
1063
|
+
console.log(chalk_1.default.yellow('⚠ HAProxy found but not accessible, verifying...'));
|
|
1064
|
+
// Re-verify
|
|
1065
|
+
const reVerifyResult = await awsService.executeSSHCommand(connection, 'haproxy -v 2>&1 || /usr/sbin/haproxy -v 2>&1');
|
|
1066
|
+
if (!reVerifyResult.stdout.includes('HAProxy') && !reVerifyResult.stdout.includes('version')) {
|
|
1067
|
+
console.log(chalk_1.default.yellow('⚠ HAProxy verification still failing, but continuing...'));
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
// Get device password from API
|
|
1072
|
+
console.log(chalk_1.default.gray('Retrieving device credentials...'));
|
|
1073
|
+
const deviceResponse = await this.apiClient.getDevice(gatewayId);
|
|
1074
|
+
const devicePassword = deviceResponse.device?.password || '';
|
|
1075
|
+
if (!devicePassword) {
|
|
1076
|
+
throw new Error('Could not retrieve device password from API');
|
|
1077
|
+
}
|
|
1078
|
+
// Create agent configuration file
|
|
1079
|
+
console.log(chalk_1.default.gray('Creating agent configuration...'));
|
|
1080
|
+
const configManager = new config_1.ConfigManager();
|
|
1081
|
+
const userConfig = configManager.getConfig();
|
|
1082
|
+
const agentConfig = {
|
|
1083
|
+
deviceId: gatewayId,
|
|
1084
|
+
devicePassword: devicePassword,
|
|
1085
|
+
deviceType: 'gateway',
|
|
1086
|
+
apiBaseUrl: (0, urls_1.getApiBaseUrl)(),
|
|
1087
|
+
organizationId: gateway.organizationId || '',
|
|
1088
|
+
firewallEnabled: true,
|
|
1089
|
+
pollingInterval: 60000,
|
|
1090
|
+
healthCheckTimeout: 5000,
|
|
1091
|
+
maxRetries: 3,
|
|
1092
|
+
logLevel: debug ? 'debug' : 'info',
|
|
1093
|
+
updateEnabled: true,
|
|
1094
|
+
updateCheckInterval: 3600000,
|
|
1095
|
+
wireguardMode: userConfig.wireguardMode || 'kernel',
|
|
1096
|
+
wireguardGoBinary: userConfig.wireguardGoBinary || 'wireguard-go'
|
|
1097
|
+
};
|
|
1098
|
+
const configFile = `/tmp/agent-config-${Date.now()}.json`;
|
|
1099
|
+
fs.writeFileSync(configFile, JSON.stringify(agentConfig, null, 2));
|
|
1100
|
+
try {
|
|
1101
|
+
console.log(chalk_1.default.gray('Uploading agent configuration...'));
|
|
1102
|
+
// Upload to user's home directory first (where we have write permissions)
|
|
1103
|
+
// Then move to /opt/edgible-agent with sudo
|
|
1104
|
+
const homeDir = connection.username === 'ec2-user' ? '/home/ec2-user' : `/home/${connection.username}`;
|
|
1105
|
+
const remoteHomeConfigPath = `${homeDir}/agent-config-${Date.now()}.json`;
|
|
1106
|
+
const remoteConfigPath = '/opt/edgible-agent/agent.config.json';
|
|
1107
|
+
console.log(chalk_1.default.gray(` Local file: ${configFile}`));
|
|
1108
|
+
console.log(chalk_1.default.gray(` Uploading to: ${remoteHomeConfigPath}`));
|
|
1109
|
+
try {
|
|
1110
|
+
await awsService.uploadFile(connection, configFile, remoteHomeConfigPath);
|
|
1111
|
+
console.log(chalk_1.default.green(' ✓ Config file uploaded successfully'));
|
|
1112
|
+
// Move to final location with sudo and set ownership
|
|
1113
|
+
console.log(chalk_1.default.gray(` Moving to: ${remoteConfigPath}`));
|
|
1114
|
+
const moveResult = await awsService.executeSSHCommand(connection, `sudo mv ${remoteHomeConfigPath} ${remoteConfigPath} && sudo chown root:root ${remoteConfigPath} && echo "OK" || echo "FAIL"`);
|
|
1115
|
+
if (moveResult.stdout.trim() === 'OK') {
|
|
1116
|
+
console.log(chalk_1.default.green(' ✓ Config file moved and ownership set'));
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
throw new Error(`Failed to move config file: ${moveResult.stderr || moveResult.stdout}`);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
catch (configError) {
|
|
1123
|
+
console.error(chalk_1.default.red(` ✗ Config upload failed: ${configError instanceof Error ? configError.message : String(configError)}`));
|
|
1124
|
+
throw new Error(`Failed to upload agent configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
finally {
|
|
1128
|
+
if (fs.existsSync(configFile)) {
|
|
1129
|
+
fs.unlinkSync(configFile);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
// Create systemd service file
|
|
1133
|
+
console.log(chalk_1.default.gray('Creating systemd service...'));
|
|
1134
|
+
const finalNodePath = nodePath === 'node not found' ? '/usr/bin/node' : nodePath;
|
|
1135
|
+
const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
|
|
1136
|
+
const serviceContent = `[Unit]
|
|
1137
|
+
Description=Edgible Agent
|
|
1138
|
+
After=network.target
|
|
1139
|
+
|
|
1140
|
+
[Service]
|
|
1141
|
+
Type=simple
|
|
1142
|
+
User=root
|
|
1143
|
+
WorkingDirectory=/opt/edgible-agent
|
|
1144
|
+
ExecStart=${finalNodePath} /opt/edgible-agent/index.js start -c /opt/edgible-agent/agent.config.json
|
|
1145
|
+
Restart=always
|
|
1146
|
+
RestartSec=10
|
|
1147
|
+
Environment=NODE_ENV=production
|
|
1148
|
+
Environment=EDGIBLE_CONFIG_PATH=${systemAgentPath}
|
|
1149
|
+
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
1150
|
+
|
|
1151
|
+
[Install]
|
|
1152
|
+
WantedBy=multi-user.target`;
|
|
1153
|
+
const serviceFile = `/tmp/edgible-agent-${Date.now()}.service`;
|
|
1154
|
+
fs.writeFileSync(serviceFile, serviceContent);
|
|
1155
|
+
try {
|
|
1156
|
+
await awsService.uploadFile(connection, serviceFile, '/tmp/edgible-agent.service');
|
|
1157
|
+
await awsService.executeSSHCommand(connection, 'sudo mv /tmp/edgible-agent.service /etc/systemd/system/edgible-agent.service');
|
|
1158
|
+
await awsService.executeSSHCommand(connection, 'sudo systemctl daemon-reload');
|
|
1159
|
+
await awsService.executeSSHCommand(connection, 'sudo systemctl enable edgible-agent.service');
|
|
1160
|
+
}
|
|
1161
|
+
finally {
|
|
1162
|
+
if (fs.existsSync(serviceFile)) {
|
|
1163
|
+
fs.unlinkSync(serviceFile);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
// Upload or download agent code
|
|
1167
|
+
if (installFromLocal) {
|
|
1168
|
+
// Use existing local upload behavior
|
|
1169
|
+
// Gateway resync always uses gateway build
|
|
1170
|
+
console.log(chalk_1.default.gray('Uploading agent code from local directory...'));
|
|
1171
|
+
const agentV2Path = path.resolve(__dirname, '../../../agent-v2');
|
|
1172
|
+
const agentDistPath = path.join(agentV2Path, 'dist');
|
|
1173
|
+
const agentPackageJsonPath = path.join(agentV2Path, 'package.json');
|
|
1174
|
+
// Ensure gateway build exists (gateway resync always needs gateway code)
|
|
1175
|
+
const { execSync } = require('child_process');
|
|
1176
|
+
const gatewayCodePath = path.join(agentDistPath, 'gateway');
|
|
1177
|
+
const hasGatewayCode = fs.existsSync(gatewayCodePath);
|
|
1178
|
+
if (!fs.existsSync(agentDistPath) || !hasGatewayCode) {
|
|
1179
|
+
console.log(chalk_1.default.yellow('⚠ Building gateway agent (required for gateway resync)...'));
|
|
1180
|
+
try {
|
|
1181
|
+
execSync('npm run build:gateway', {
|
|
1182
|
+
cwd: agentV2Path,
|
|
1183
|
+
stdio: 'inherit'
|
|
1184
|
+
});
|
|
1185
|
+
console.log(chalk_1.default.green('✓ Gateway build completed'));
|
|
1186
|
+
}
|
|
1187
|
+
catch (buildError) {
|
|
1188
|
+
throw new Error(`Failed to build gateway agent: ${buildError instanceof Error ? buildError.message : String(buildError)}\n\n` +
|
|
1189
|
+
`Please build manually:\n` +
|
|
1190
|
+
` cd agent-v2\n` +
|
|
1191
|
+
` npm run build:gateway`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (!fs.existsSync(agentDistPath)) {
|
|
1195
|
+
throw new Error('Agent distribution not found. This feature requires development environment setup. Please use S3 download instead.');
|
|
1196
|
+
}
|
|
1197
|
+
if (!fs.existsSync(agentPackageJsonPath)) {
|
|
1198
|
+
throw new Error('Agent package.json not found.');
|
|
1199
|
+
}
|
|
1200
|
+
// Create a tarball of the dist directory and package.json
|
|
1201
|
+
const tarPath = `/tmp/agent-dist-${Date.now()}.tar.gz`;
|
|
1202
|
+
try {
|
|
1203
|
+
console.log(chalk_1.default.gray('Creating agent distribution tarball...'));
|
|
1204
|
+
console.log(chalk_1.default.gray(` Source dist: ${agentDistPath}`));
|
|
1205
|
+
console.log(chalk_1.default.gray(` Source package.json: ${agentPackageJsonPath}`));
|
|
1206
|
+
// Create tarball with dist contents at root level and package.json
|
|
1207
|
+
const tempDir = `/tmp/agent-upload-${Date.now()}`;
|
|
1208
|
+
console.log(chalk_1.default.gray(` Creating temp directory: ${tempDir}`));
|
|
1209
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1210
|
+
fs.copyFileSync(agentPackageJsonPath, path.join(tempDir, 'package.json'));
|
|
1211
|
+
// Copy contents of dist/ directly to tempDir root (so index.js ends up at /opt/edgible-agent/index.js)
|
|
1212
|
+
// Copy all files from dist including hidden files, but exclude . and ..
|
|
1213
|
+
execSync(`sh -c 'cd "${agentDistPath}" && find . -mindepth 1 -maxdepth 1 -exec cp -r {} "${tempDir}/" \\;'`, { stdio: 'inherit' });
|
|
1214
|
+
console.log(chalk_1.default.gray(` Creating tarball: ${tarPath}`));
|
|
1215
|
+
execSync(`cd "${tempDir}" && tar -czf "${tarPath}" .`, { stdio: 'inherit' });
|
|
1216
|
+
// Verify tarball was created
|
|
1217
|
+
if (!fs.existsSync(tarPath)) {
|
|
1218
|
+
throw new Error(`Failed to create tarball at ${tarPath}`);
|
|
1219
|
+
}
|
|
1220
|
+
const tarStats = fs.statSync(tarPath);
|
|
1221
|
+
const tarSizeMB = (tarStats.size / (1024 * 1024)).toFixed(2);
|
|
1222
|
+
console.log(chalk_1.default.gray(` Tarball created: ${tarSizeMB} MB`));
|
|
1223
|
+
// Clean up temp directory
|
|
1224
|
+
console.log(chalk_1.default.gray(' Cleaning up temp directory...'));
|
|
1225
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1226
|
+
// Upload tarball to user's home directory first (where we have write permissions)
|
|
1227
|
+
// Then move to /tmp with sudo if needed
|
|
1228
|
+
// Use explicit home directory path since SFTP might not expand ~
|
|
1229
|
+
const homeDir = connection.username === 'ec2-user' ? '/home/ec2-user' : `/home/${connection.username}`;
|
|
1230
|
+
const remoteHomePath = `${homeDir}/agent-dist-${Date.now()}.tar.gz`;
|
|
1231
|
+
const remoteTmpPath = `/tmp/agent-dist.tar.gz`;
|
|
1232
|
+
console.log(chalk_1.default.gray('Checking remote directory permissions...'));
|
|
1233
|
+
try {
|
|
1234
|
+
const homeDirCheck = await awsService.executeSSHCommand(connection, `test -d ${homeDir} && test -w ${homeDir} && echo "OK" || echo "FAIL"`);
|
|
1235
|
+
if (homeDirCheck.stdout.trim() !== 'OK') {
|
|
1236
|
+
console.log(chalk_1.default.yellow(` Warning: Home directory ${homeDir} may not be writable`));
|
|
1237
|
+
console.log(chalk_1.default.gray(` Attempting to verify with ls -ld: ${homeDir}`));
|
|
1238
|
+
const lsResult = await awsService.executeSSHCommand(connection, `ls -ld ${homeDir} 2>&1 || echo "directory_not_found"`);
|
|
1239
|
+
console.log(chalk_1.default.gray(` Directory info: ${lsResult.stdout.trim()}`));
|
|
1240
|
+
}
|
|
1241
|
+
else {
|
|
1242
|
+
console.log(chalk_1.default.gray(` ✓ Home directory ${homeDir} is writable`));
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
catch (checkError) {
|
|
1246
|
+
console.log(chalk_1.default.yellow(` Warning: Could not verify directory permissions: ${checkError instanceof Error ? checkError.message : String(checkError)}`));
|
|
1247
|
+
}
|
|
1248
|
+
console.log(chalk_1.default.blue('Uploading tarball to remote server...'));
|
|
1249
|
+
console.log(chalk_1.default.gray(` Local file: ${tarPath}`));
|
|
1250
|
+
console.log(chalk_1.default.gray(` Remote path: ${remoteHomePath}`));
|
|
1251
|
+
console.log(chalk_1.default.gray(` File size: ${tarSizeMB} MB`));
|
|
1252
|
+
try {
|
|
1253
|
+
await awsService.uploadFile(connection, tarPath, remoteHomePath);
|
|
1254
|
+
console.log(chalk_1.default.green(' ✓ Upload completed successfully'));
|
|
1255
|
+
// Verify file was uploaded
|
|
1256
|
+
console.log(chalk_1.default.gray(' Verifying uploaded file...'));
|
|
1257
|
+
const verifyResult = await awsService.executeSSHCommand(connection, `test -f ${remoteHomePath} && ls -lh ${remoteHomePath} || echo "file_not_found"`);
|
|
1258
|
+
if (verifyResult.stdout.includes('file_not_found')) {
|
|
1259
|
+
throw new Error(`Upload verification failed: file not found at ${remoteHomePath}`);
|
|
1260
|
+
}
|
|
1261
|
+
console.log(chalk_1.default.gray(` File verified: ${verifyResult.stdout.trim()}`));
|
|
1262
|
+
}
|
|
1263
|
+
catch (uploadError) {
|
|
1264
|
+
console.error(chalk_1.default.red(` ✗ Upload failed: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`));
|
|
1265
|
+
throw new Error(`Failed to upload file to ${remoteHomePath}: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`);
|
|
1266
|
+
}
|
|
1267
|
+
// Move to /tmp using sudo (in case /tmp has permission restrictions)
|
|
1268
|
+
console.log(chalk_1.default.gray(`Moving file from ${remoteHomePath} to ${remoteTmpPath}...`));
|
|
1269
|
+
try {
|
|
1270
|
+
const moveResult = await awsService.executeSSHCommand(connection, `sudo mv ${remoteHomePath} ${remoteTmpPath} && sudo chmod 644 ${remoteTmpPath} && echo "OK" || echo "FAIL"`);
|
|
1271
|
+
if (moveResult.stdout.trim() === 'OK') {
|
|
1272
|
+
console.log(chalk_1.default.green(' ✓ File moved successfully'));
|
|
1273
|
+
}
|
|
1274
|
+
else {
|
|
1275
|
+
throw new Error(`Failed to move file: ${moveResult.stderr || moveResult.stdout}`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
catch (moveError) {
|
|
1279
|
+
console.error(chalk_1.default.red(` ✗ Move failed: ${moveError instanceof Error ? moveError.message : String(moveError)}`));
|
|
1280
|
+
throw new Error(`Failed to move file from ${remoteHomePath} to ${remoteTmpPath}: ${moveError instanceof Error ? moveError.message : String(moveError)}`);
|
|
1281
|
+
}
|
|
1282
|
+
// Extract on remote server
|
|
1283
|
+
console.log(chalk_1.default.gray('Extracting agent code...'));
|
|
1284
|
+
try {
|
|
1285
|
+
const extractResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo tar -xzf /tmp/agent-dist.tar.gz && sudo rm /tmp/agent-dist.tar.gz && echo "OK" || echo "FAIL"');
|
|
1286
|
+
if (extractResult.stdout.trim() === 'OK') {
|
|
1287
|
+
console.log(chalk_1.default.green(' ✓ Extraction completed successfully'));
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
console.log(chalk_1.default.yellow(` Warning: Extraction may have failed: ${extractResult.stderr || extractResult.stdout}`));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
catch (extractError) {
|
|
1294
|
+
console.error(chalk_1.default.red(` ✗ Extraction failed: ${extractError instanceof Error ? extractError.message : String(extractError)}`));
|
|
1295
|
+
throw new Error(`Failed to extract agent code: ${extractError instanceof Error ? extractError.message : String(extractError)}`);
|
|
1296
|
+
}
|
|
1297
|
+
// Install production dependencies (run as root since directory is owned by root)
|
|
1298
|
+
console.log(chalk_1.default.gray('Installing agent dependencies...'));
|
|
1299
|
+
try {
|
|
1300
|
+
const installResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo npm install --production --no-save');
|
|
1301
|
+
if (installResult.exitCode === 0) {
|
|
1302
|
+
console.log(chalk_1.default.green(' ✓ Dependencies installed successfully'));
|
|
1303
|
+
}
|
|
1304
|
+
else {
|
|
1305
|
+
console.log(chalk_1.default.yellow(` Warning: npm install exited with code ${installResult.exitCode}`));
|
|
1306
|
+
console.log(chalk_1.default.gray(` stdout: ${installResult.stdout}`));
|
|
1307
|
+
if (installResult.stderr) {
|
|
1308
|
+
console.log(chalk_1.default.gray(` stderr: ${installResult.stderr}`));
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
catch (installError) {
|
|
1313
|
+
console.error(chalk_1.default.red(` ✗ Dependency installation failed: ${installError instanceof Error ? installError.message : String(installError)}`));
|
|
1314
|
+
throw new Error(`Failed to install dependencies: ${installError instanceof Error ? installError.message : String(installError)}`);
|
|
1315
|
+
}
|
|
1316
|
+
// Ensure index.js is executable
|
|
1317
|
+
console.log(chalk_1.default.gray('Setting file permissions...'));
|
|
1318
|
+
try {
|
|
1319
|
+
await awsService.executeSSHCommand(connection, 'sudo chmod +x /opt/edgible-agent/index.js || true');
|
|
1320
|
+
// Set proper ownership (root will own everything)
|
|
1321
|
+
await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent');
|
|
1322
|
+
// Create logs directory for root user
|
|
1323
|
+
const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
|
|
1324
|
+
await awsService.executeSSHCommand(connection, `sudo mkdir -p ${systemAgentPath}/logs`);
|
|
1325
|
+
await awsService.executeSSHCommand(connection, `sudo chown -R root:root ${PathResolver_1.PathResolver.getSystemDataPath()}`);
|
|
1326
|
+
await awsService.executeSSHCommand(connection, `sudo chmod -R 755 ${PathResolver_1.PathResolver.getSystemDataPath()}`);
|
|
1327
|
+
console.log(chalk_1.default.green(' ✓ Permissions and ownership set correctly'));
|
|
1328
|
+
}
|
|
1329
|
+
catch (permError) {
|
|
1330
|
+
console.error(chalk_1.default.red(` ✗ Failed to set permissions: ${permError instanceof Error ? permError.message : String(permError)}`));
|
|
1331
|
+
throw new Error(`Failed to set file permissions: ${permError instanceof Error ? permError.message : String(permError)}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
finally {
|
|
1335
|
+
if (fs.existsSync(tarPath)) {
|
|
1336
|
+
fs.unlinkSync(tarPath);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
else {
|
|
1341
|
+
// Download from CloudFront on remote server
|
|
1342
|
+
console.log(chalk_1.default.gray('Downloading agent from CloudFront...'));
|
|
1343
|
+
const distributionUrl = (0, urls_1.getDistributionUrl)();
|
|
1344
|
+
const version = agentVersion || 'latest';
|
|
1345
|
+
// Gateway resync always uses gateway build
|
|
1346
|
+
// Build CloudFront URL with gateway device type prefix
|
|
1347
|
+
// Format: {distributionUrl}/gateway/{version}.zip
|
|
1348
|
+
const downloadUrl = `${distributionUrl}/gateway/${version}.zip`;
|
|
1349
|
+
console.log(chalk_1.default.gray(` Distribution: ${distributionUrl}`));
|
|
1350
|
+
console.log(chalk_1.default.gray(` Device Type: gateway`));
|
|
1351
|
+
console.log(chalk_1.default.gray(` Version: ${version}`));
|
|
1352
|
+
console.log(chalk_1.default.gray(` URL: ${downloadUrl}`));
|
|
1353
|
+
// Check if unzip is installed, install if needed
|
|
1354
|
+
console.log(chalk_1.default.gray('Checking for unzip utility...'));
|
|
1355
|
+
const unzipCheck = await awsService.executeSSHCommand(connection, 'which unzip || echo "unzip not found"');
|
|
1356
|
+
if (unzipCheck.stdout.trim() === 'unzip not found') {
|
|
1357
|
+
console.log(chalk_1.default.gray('Installing unzip...'));
|
|
1358
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y unzip || sudo apt-get install -y unzip');
|
|
1359
|
+
}
|
|
1360
|
+
else {
|
|
1361
|
+
console.log(chalk_1.default.gray('✓ unzip is installed'));
|
|
1362
|
+
}
|
|
1363
|
+
// Download and extract on remote server with verbose output
|
|
1364
|
+
const downloadScript = `
|
|
1365
|
+
set -e
|
|
1366
|
+
echo "Step 1: Cleaning old files..."
|
|
1367
|
+
cd /opt/edgible-agent && sudo rm -rf dist package.json node_modules index.js *.map && echo "✓ Cleaned"
|
|
1368
|
+
echo "Step 2: Downloading agent..."
|
|
1369
|
+
sudo curl -L -f --max-time 300 --connect-timeout 30 -w "Downloaded %{size_download} bytes in %{time_total}s\\n" -o /tmp/agent.zip "${downloadUrl}" && echo "✓ Downloaded"
|
|
1370
|
+
echo "Step 3: Extracting agent..."
|
|
1371
|
+
sudo unzip -o -q /tmp/agent.zip -d /opt/edgible-agent && echo "✓ Extracted"
|
|
1372
|
+
echo "Step 4: Cleaning up..."
|
|
1373
|
+
sudo rm /tmp/agent.zip && echo "✓ Cleanup complete"
|
|
1374
|
+
echo "OK"
|
|
1375
|
+
`;
|
|
1376
|
+
console.log(chalk_1.default.gray(' Downloading agent (this may take a moment)...'));
|
|
1377
|
+
try {
|
|
1378
|
+
const downloadResult = await awsService.executeSSHCommand(connection, downloadScript);
|
|
1379
|
+
// Log the output for debugging
|
|
1380
|
+
if (downloadResult.stdout) {
|
|
1381
|
+
console.log(chalk_1.default.gray(' Output:'), downloadResult.stdout.trim());
|
|
1382
|
+
}
|
|
1383
|
+
if (downloadResult.stderr) {
|
|
1384
|
+
console.log(chalk_1.default.yellow(' Stderr:'), downloadResult.stderr.trim());
|
|
1385
|
+
}
|
|
1386
|
+
if (downloadResult.stdout.includes('OK')) {
|
|
1387
|
+
console.log(chalk_1.default.green(' ✓ Download and extraction completed successfully'));
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
throw new Error(`Download failed: ${downloadResult.stderr || downloadResult.stdout}`);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
catch (downloadError) {
|
|
1394
|
+
console.error(chalk_1.default.red(` ✗ Download failed: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`));
|
|
1395
|
+
throw downloadError;
|
|
1396
|
+
}
|
|
1397
|
+
// Verify dependencies are present (already included in zip)
|
|
1398
|
+
console.log(chalk_1.default.gray('Verifying agent dependencies...'));
|
|
1399
|
+
try {
|
|
1400
|
+
const verifyResult = await awsService.executeSSHCommand(connection, 'test -d /opt/edgible-agent/node_modules/commander && echo "OK" || echo "MISSING"');
|
|
1401
|
+
if (verifyResult.stdout.trim() === 'OK') {
|
|
1402
|
+
console.log(chalk_1.default.green(' ✓ Dependencies verified (included in package)'));
|
|
1403
|
+
}
|
|
1404
|
+
else {
|
|
1405
|
+
console.log(chalk_1.default.yellow(' ⚠ Dependencies missing, installing...'));
|
|
1406
|
+
const installResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo npm install --production --no-save --no-audit --no-fund --loglevel=error');
|
|
1407
|
+
if (installResult.exitCode === 0) {
|
|
1408
|
+
console.log(chalk_1.default.green(' ✓ Dependencies installed successfully'));
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
console.log(chalk_1.default.yellow(` Warning: npm install exited with code ${installResult.exitCode}`));
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
catch (verifyError) {
|
|
1416
|
+
console.log(chalk_1.default.yellow(` ⚠ Could not verify dependencies: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`));
|
|
1417
|
+
}
|
|
1418
|
+
// Set file permissions
|
|
1419
|
+
console.log(chalk_1.default.gray('Setting file permissions...'));
|
|
1420
|
+
try {
|
|
1421
|
+
await awsService.executeSSHCommand(connection, 'sudo chmod +x /opt/edgible-agent/index.js || true');
|
|
1422
|
+
await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent');
|
|
1423
|
+
const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
|
|
1424
|
+
await awsService.executeSSHCommand(connection, `sudo mkdir -p ${systemAgentPath}/logs`);
|
|
1425
|
+
await awsService.executeSSHCommand(connection, `sudo chown -R root:root ${PathResolver_1.PathResolver.getSystemDataPath()}`);
|
|
1426
|
+
await awsService.executeSSHCommand(connection, `sudo chmod -R 755 ${PathResolver_1.PathResolver.getSystemDataPath()}`);
|
|
1427
|
+
console.log(chalk_1.default.green(' ✓ Permissions and ownership set correctly'));
|
|
1428
|
+
}
|
|
1429
|
+
catch (permError) {
|
|
1430
|
+
console.error(chalk_1.default.red(` ✗ Failed to set permissions: ${permError instanceof Error ? permError.message : String(permError)}`));
|
|
1431
|
+
throw new Error(`Failed to set file permissions: ${permError instanceof Error ? permError.message : String(permError)}`);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
// Start agent service
|
|
1435
|
+
console.log(chalk_1.default.gray('Starting agent service...'));
|
|
1436
|
+
try {
|
|
1437
|
+
await awsService.executeSSHCommand(connection, 'sudo systemctl restart edgible-agent');
|
|
1438
|
+
}
|
|
1439
|
+
catch (startError) {
|
|
1440
|
+
console.error(chalk_1.default.red('Failed to start service:'), startError);
|
|
1441
|
+
// Check service status and logs
|
|
1442
|
+
const statusResult = await awsService.executeSSHCommand(connection, 'sudo systemctl status edgible-agent');
|
|
1443
|
+
console.log(chalk_1.default.red('Service status:'), statusResult.stdout);
|
|
1444
|
+
console.log(chalk_1.default.red('Service stderr:'), statusResult.stderr);
|
|
1445
|
+
throw new Error('Agent service failed to start');
|
|
1446
|
+
}
|
|
1447
|
+
// Verify agent is running
|
|
1448
|
+
const statusResult = await awsService.executeSSHCommand(connection, 'sudo systemctl is-active edgible-agent');
|
|
1449
|
+
if (statusResult.stdout.trim() !== 'active') {
|
|
1450
|
+
// Get more details about why it failed
|
|
1451
|
+
const statusDetails = await awsService.executeSSHCommand(connection, 'sudo systemctl status edgible-agent');
|
|
1452
|
+
console.log(chalk_1.default.red('Service status details:'), statusDetails.stdout);
|
|
1453
|
+
throw new Error('Agent service failed to start');
|
|
1454
|
+
}
|
|
1455
|
+
console.log(chalk_1.default.green('✓ Agent resynced successfully!'));
|
|
1456
|
+
// Reboot if requested
|
|
1457
|
+
let rebooted = false;
|
|
1458
|
+
if (reboot) {
|
|
1459
|
+
console.log(chalk_1.default.blue('Rebooting gateway...'));
|
|
1460
|
+
try {
|
|
1461
|
+
const rebootResult = await this.executeManagedGatewayCommand(gatewayId, 'sudo reboot');
|
|
1462
|
+
// Note: The command may not return successfully because the gateway reboots
|
|
1463
|
+
// and disconnects the SSH session. We check if we got an error that suggests
|
|
1464
|
+
// the connection was lost (which is expected during reboot)
|
|
1465
|
+
if (rebootResult.success || (rebootResult.error && rebootResult.error.includes('ECONNRESET'))) {
|
|
1466
|
+
console.log(chalk_1.default.green('✓ Reboot command sent successfully'));
|
|
1467
|
+
console.log(chalk_1.default.yellow('⚠ Gateway is rebooting. SSH connection will be lost.'));
|
|
1468
|
+
console.log(chalk_1.default.gray(' The gateway should be back online in a few minutes.'));
|
|
1469
|
+
rebooted = true;
|
|
1470
|
+
}
|
|
1471
|
+
else {
|
|
1472
|
+
console.log(chalk_1.default.yellow('⚠ Reboot command may have failed, but continuing...'));
|
|
1473
|
+
if (rebootResult.error) {
|
|
1474
|
+
console.log(chalk_1.default.gray(` Error: ${rebootResult.error}`));
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
catch (rebootError) {
|
|
1479
|
+
// If the error is a connection reset, that's expected during reboot
|
|
1480
|
+
if (rebootError instanceof Error && (rebootError.message.includes('ECONNRESET') || rebootError.message.includes('connection'))) {
|
|
1481
|
+
console.log(chalk_1.default.green('✓ Reboot command sent (connection lost as expected)'));
|
|
1482
|
+
console.log(chalk_1.default.yellow('⚠ Gateway is rebooting.'));
|
|
1483
|
+
rebooted = true;
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
console.log(chalk_1.default.yellow(`⚠ Error sending reboot command: ${rebootError instanceof Error ? rebootError.message : String(rebootError)}`));
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return {
|
|
1491
|
+
message: 'Agent resynced successfully',
|
|
1492
|
+
success: true,
|
|
1493
|
+
agentVersion: agentVersion || 'latest',
|
|
1494
|
+
syncTimestamp: new Date().toISOString(),
|
|
1495
|
+
logsWiped: logsWiped,
|
|
1496
|
+
rebooted: rebooted
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
finally {
|
|
1500
|
+
// Clean up temp key file
|
|
1501
|
+
try {
|
|
1502
|
+
if (fs.existsSync(tempKeyPath)) {
|
|
1503
|
+
fs.unlinkSync(tempKeyPath);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
catch (cleanupError) {
|
|
1507
|
+
// Ignore cleanup errors
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
catch (error) {
|
|
1512
|
+
console.error(chalk_1.default.red('Error resyncing managed gateway agent:'), error);
|
|
1513
|
+
throw error;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
async resyncGatewayAgent(gatewayId, agentVersion, installFromLocal) {
|
|
1517
|
+
try {
|
|
1518
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
1519
|
+
await this.attemptAutoRelogin();
|
|
1520
|
+
const configManager = new config_1.ConfigManager();
|
|
1521
|
+
const gatewayInfo = configManager.getGateway(gatewayId);
|
|
1522
|
+
if (!gatewayInfo) {
|
|
1523
|
+
throw new Error('Gateway not found in local configuration');
|
|
1524
|
+
}
|
|
1525
|
+
// Load SSH key
|
|
1526
|
+
const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
|
|
1527
|
+
const privateKey = awsService.loadSSHKey(gatewayInfo.keyPath);
|
|
1528
|
+
// Wait for instance to be ready for SSH
|
|
1529
|
+
const isReady = await awsService.waitForSSHReady(gatewayInfo.ec2InstanceId);
|
|
1530
|
+
if (!isReady) {
|
|
1531
|
+
throw new Error('Instance is not ready for SSH connections');
|
|
1532
|
+
}
|
|
1533
|
+
// Create SSH connection
|
|
1534
|
+
const connection = {
|
|
1535
|
+
host: gatewayInfo.publicIp,
|
|
1536
|
+
port: 22,
|
|
1537
|
+
username: 'ec2-user',
|
|
1538
|
+
privateKey: privateKey
|
|
1539
|
+
};
|
|
1540
|
+
console.log(chalk_1.default.blue('Connecting to gateway...'));
|
|
1541
|
+
console.log(chalk_1.default.gray(`SSH Host: ${connection.host}:${connection.port}`));
|
|
1542
|
+
console.log(chalk_1.default.gray(`SSH User: ${connection.username}`));
|
|
1543
|
+
console.log(chalk_1.default.gray(`SSH Key: ${gatewayInfo.keyPath}`));
|
|
1544
|
+
// Test SSH connection first
|
|
1545
|
+
console.log(chalk_1.default.gray('Testing SSH connection...'));
|
|
1546
|
+
try {
|
|
1547
|
+
await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
|
|
1548
|
+
console.log(chalk_1.default.green('✓ SSH connection established'));
|
|
1549
|
+
}
|
|
1550
|
+
catch (sshError) {
|
|
1551
|
+
console.error(chalk_1.default.red('✗ SSH connection failed:'), sshError);
|
|
1552
|
+
throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
|
|
1553
|
+
}
|
|
1554
|
+
// Stop agent service
|
|
1555
|
+
console.log(chalk_1.default.gray('Stopping agent service...'));
|
|
1556
|
+
await awsService.executeSSHCommand(connection, 'sudo systemctl stop edgible-agent');
|
|
1557
|
+
// Ensure the agent directory exists
|
|
1558
|
+
console.log(chalk_1.default.gray('Creating agent directory...'));
|
|
1559
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /opt/edgible-agent');
|
|
1560
|
+
await awsService.executeSSHCommand(connection, 'sudo chown root:root /opt/edgible-agent');
|
|
1561
|
+
// Check Node.js installation and path
|
|
1562
|
+
console.log(chalk_1.default.gray('Checking Node.js installation...'));
|
|
1563
|
+
const nodeCheck = await awsService.executeSSHCommand(connection, 'which node || echo "node not found"');
|
|
1564
|
+
const nodePath = nodeCheck.stdout.trim();
|
|
1565
|
+
console.log(chalk_1.default.gray(`Node.js path: ${nodePath}`));
|
|
1566
|
+
if (nodePath === 'node not found') {
|
|
1567
|
+
console.log(chalk_1.default.gray('Installing Node.js...'));
|
|
1568
|
+
await awsService.executeSSHCommand(connection, 'curl -fsSL https://rpm.nodesource.com/setup_18.x | sudo bash -');
|
|
1569
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y nodejs');
|
|
1570
|
+
}
|
|
1571
|
+
// Check and install required system dependencies
|
|
1572
|
+
console.log(chalk_1.default.gray('Checking system dependencies...'));
|
|
1573
|
+
// Check iptables
|
|
1574
|
+
const iptablesCheck = await awsService.executeSSHCommand(connection, 'which iptables || echo "iptables not found"');
|
|
1575
|
+
if (iptablesCheck.stdout.trim() === 'iptables not found') {
|
|
1576
|
+
console.log(chalk_1.default.gray('Installing iptables...'));
|
|
1577
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y iptables');
|
|
1578
|
+
}
|
|
1579
|
+
else {
|
|
1580
|
+
console.log(chalk_1.default.gray('✓ iptables is installed'));
|
|
1581
|
+
}
|
|
1582
|
+
// Check WireGuard
|
|
1583
|
+
const wireguardCheck = await awsService.executeSSHCommand(connection, 'which wg || echo "wireguard not found"');
|
|
1584
|
+
if (wireguardCheck.stdout.trim() === 'wireguard not found') {
|
|
1585
|
+
console.log(chalk_1.default.gray('Installing WireGuard...'));
|
|
1586
|
+
// For Amazon Linux 2, we need to install WireGuard from EPEL
|
|
1587
|
+
await awsService.executeSSHCommand(connection, 'sudo amazon-linux-extras install epel -y || sudo yum install -y epel-release');
|
|
1588
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y wireguard-tools');
|
|
1589
|
+
// Load the kernel module (if not already loaded)
|
|
1590
|
+
await awsService.executeSSHCommand(connection, 'sudo modprobe wireguard || true');
|
|
1591
|
+
}
|
|
1592
|
+
else {
|
|
1593
|
+
console.log(chalk_1.default.gray('✓ WireGuard is installed'));
|
|
1594
|
+
}
|
|
1595
|
+
// Check HAProxy (gateway devices use HAProxy instead of Caddy)
|
|
1596
|
+
const haproxyCheck = await awsService.executeSSHCommand(connection, 'which haproxy 2>/dev/null || test -f /usr/sbin/haproxy && echo "haproxy found" || echo "haproxy not found"');
|
|
1597
|
+
if (haproxyCheck.stdout.trim() === 'haproxy not found' || (haproxyCheck.stdout.trim() === '' && !haproxyCheck.stdout.includes('haproxy found'))) {
|
|
1598
|
+
console.log(chalk_1.default.gray('Installing HAProxy...'));
|
|
1599
|
+
// Install HAProxy via package manager
|
|
1600
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y haproxy');
|
|
1601
|
+
// Create HAProxy config directory
|
|
1602
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /etc/haproxy');
|
|
1603
|
+
// Create HAProxy runtime socket directory (required for admin socket)
|
|
1604
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /var/run/haproxy');
|
|
1605
|
+
await awsService.executeSSHCommand(connection, 'sudo chmod 755 /var/run/haproxy');
|
|
1606
|
+
// Set capabilities for HAProxy to bind to privileged ports
|
|
1607
|
+
await awsService.executeSSHCommand(connection, 'sudo setcap cap_net_bind_service=+ep /usr/sbin/haproxy || true');
|
|
1608
|
+
// Create HAProxy chroot directory
|
|
1609
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /var/lib/haproxy');
|
|
1610
|
+
await awsService.executeSSHCommand(connection, 'sudo chown haproxy:haproxy /var/lib/haproxy || sudo chown root:root /var/lib/haproxy || true');
|
|
1611
|
+
// Verify installation
|
|
1612
|
+
const verifyResult = await awsService.executeSSHCommand(connection, '/usr/sbin/haproxy -v 2>&1');
|
|
1613
|
+
if (!verifyResult.stdout.includes('HAProxy') && !verifyResult.stdout.includes('version')) {
|
|
1614
|
+
throw new Error(`HAProxy installation verification failed. Output: ${verifyResult.stdout}`);
|
|
1615
|
+
}
|
|
1616
|
+
console.log(chalk_1.default.green('✓ HAProxy installed successfully'));
|
|
1617
|
+
}
|
|
1618
|
+
else {
|
|
1619
|
+
console.log(chalk_1.default.gray('✓ HAProxy is installed'));
|
|
1620
|
+
// Verify it's accessible
|
|
1621
|
+
const verifyResult = await awsService.executeSSHCommand(connection, 'haproxy -v 2>&1 || /usr/sbin/haproxy -v 2>&1');
|
|
1622
|
+
if (verifyResult.stdout.includes('command not found') && !verifyResult.stdout.includes('HAProxy') && !verifyResult.stdout.includes('version')) {
|
|
1623
|
+
console.log(chalk_1.default.yellow('⚠ HAProxy found but not accessible, verifying...'));
|
|
1624
|
+
// Re-verify
|
|
1625
|
+
const reVerifyResult = await awsService.executeSSHCommand(connection, 'haproxy -v 2>&1 || /usr/sbin/haproxy -v 2>&1');
|
|
1626
|
+
if (!reVerifyResult.stdout.includes('HAProxy') && !reVerifyResult.stdout.includes('version')) {
|
|
1627
|
+
console.log(chalk_1.default.yellow('⚠ HAProxy verification still failing, but continuing...'));
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
// Get device password from API
|
|
1632
|
+
console.log(chalk_1.default.gray('Retrieving device credentials...'));
|
|
1633
|
+
const deviceResponse = await this.apiClient.getDevice(gatewayInfo.deviceId);
|
|
1634
|
+
const devicePassword = deviceResponse.device?.password || '';
|
|
1635
|
+
if (!devicePassword) {
|
|
1636
|
+
throw new Error('Could not retrieve device password from API');
|
|
1637
|
+
}
|
|
1638
|
+
// Create agent configuration file
|
|
1639
|
+
console.log(chalk_1.default.gray('Creating agent configuration...'));
|
|
1640
|
+
const userConfig = configManager.getConfig();
|
|
1641
|
+
const agentConfig = {
|
|
1642
|
+
deviceId: gatewayInfo.deviceId,
|
|
1643
|
+
devicePassword: devicePassword,
|
|
1644
|
+
deviceType: 'gateway',
|
|
1645
|
+
apiBaseUrl: (0, urls_1.getApiBaseUrl)(),
|
|
1646
|
+
organizationId: userConfig.organizationId || '',
|
|
1647
|
+
firewallEnabled: true,
|
|
1648
|
+
pollingInterval: 60000,
|
|
1649
|
+
healthCheckTimeout: 5000,
|
|
1650
|
+
maxRetries: 3,
|
|
1651
|
+
logLevel: 'info',
|
|
1652
|
+
updateEnabled: true,
|
|
1653
|
+
updateCheckInterval: 3600000,
|
|
1654
|
+
wireguardMode: userConfig.wireguardMode || 'kernel',
|
|
1655
|
+
wireguardGoBinary: userConfig.wireguardGoBinary || 'wireguard-go'
|
|
1656
|
+
};
|
|
1657
|
+
const configFile = `/tmp/agent-config-${Date.now()}.json`;
|
|
1658
|
+
fs.writeFileSync(configFile, JSON.stringify(agentConfig, null, 2));
|
|
1659
|
+
try {
|
|
1660
|
+
console.log(chalk_1.default.gray('Uploading agent configuration...'));
|
|
1661
|
+
// Upload to user's home directory first (where we have write permissions)
|
|
1662
|
+
// Then move to /opt/edgible-agent with sudo
|
|
1663
|
+
const homeDir = connection.username === 'ec2-user' ? '/home/ec2-user' : `/home/${connection.username}`;
|
|
1664
|
+
const remoteHomeConfigPath = `${homeDir}/agent-config-${Date.now()}.json`;
|
|
1665
|
+
const remoteConfigPath = '/opt/edgible-agent/agent.config.json';
|
|
1666
|
+
console.log(chalk_1.default.gray(` Local file: ${configFile}`));
|
|
1667
|
+
console.log(chalk_1.default.gray(` Uploading to: ${remoteHomeConfigPath}`));
|
|
1668
|
+
try {
|
|
1669
|
+
await awsService.uploadFile(connection, configFile, remoteHomeConfigPath);
|
|
1670
|
+
console.log(chalk_1.default.green(' ✓ Config file uploaded successfully'));
|
|
1671
|
+
// Move to final location with sudo and set ownership
|
|
1672
|
+
console.log(chalk_1.default.gray(` Moving to: ${remoteConfigPath}`));
|
|
1673
|
+
const moveResult = await awsService.executeSSHCommand(connection, `sudo mv ${remoteHomeConfigPath} ${remoteConfigPath} && sudo chown root:root ${remoteConfigPath} && echo "OK" || echo "FAIL"`);
|
|
1674
|
+
if (moveResult.stdout.trim() === 'OK') {
|
|
1675
|
+
console.log(chalk_1.default.green(' ✓ Config file moved and ownership set'));
|
|
1676
|
+
}
|
|
1677
|
+
else {
|
|
1678
|
+
throw new Error(`Failed to move config file: ${moveResult.stderr || moveResult.stdout}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
catch (configError) {
|
|
1682
|
+
console.error(chalk_1.default.red(` ✗ Config upload failed: ${configError instanceof Error ? configError.message : String(configError)}`));
|
|
1683
|
+
throw new Error(`Failed to upload agent configuration: ${configError instanceof Error ? configError.message : String(configError)}`);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
finally {
|
|
1687
|
+
if (fs.existsSync(configFile)) {
|
|
1688
|
+
fs.unlinkSync(configFile);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
// Create systemd service file
|
|
1692
|
+
console.log(chalk_1.default.gray('Creating systemd service...'));
|
|
1693
|
+
const finalNodePath = nodePath === 'node not found' ? '/usr/bin/node' : nodePath;
|
|
1694
|
+
const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
|
|
1695
|
+
const serviceContent = `[Unit]
|
|
1696
|
+
Description=Edgible Agent
|
|
1697
|
+
After=network.target
|
|
1698
|
+
|
|
1699
|
+
[Service]
|
|
1700
|
+
Type=simple
|
|
1701
|
+
User=root
|
|
1702
|
+
WorkingDirectory=/opt/edgible-agent
|
|
1703
|
+
ExecStart=${finalNodePath} /opt/edgible-agent/index.js start -c /opt/edgible-agent/agent.config.json
|
|
1704
|
+
Restart=always
|
|
1705
|
+
RestartSec=10
|
|
1706
|
+
Environment=NODE_ENV=production
|
|
1707
|
+
Environment=EDGIBLE_CONFIG_PATH=${systemAgentPath}
|
|
1708
|
+
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
1709
|
+
|
|
1710
|
+
[Install]
|
|
1711
|
+
WantedBy=multi-user.target`;
|
|
1712
|
+
const serviceFile = `/tmp/edgible-agent-${Date.now()}.service`;
|
|
1713
|
+
fs.writeFileSync(serviceFile, serviceContent);
|
|
1714
|
+
try {
|
|
1715
|
+
await awsService.uploadFile(connection, serviceFile, '/tmp/edgible-agent.service');
|
|
1716
|
+
await awsService.executeSSHCommand(connection, 'sudo mv /tmp/edgible-agent.service /etc/systemd/system/edgible-agent.service');
|
|
1717
|
+
await awsService.executeSSHCommand(connection, 'sudo systemctl daemon-reload');
|
|
1718
|
+
await awsService.executeSSHCommand(connection, 'sudo systemctl enable edgible-agent.service');
|
|
1719
|
+
}
|
|
1720
|
+
finally {
|
|
1721
|
+
if (fs.existsSync(serviceFile)) {
|
|
1722
|
+
fs.unlinkSync(serviceFile);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
// Upload or download agent code
|
|
1726
|
+
if (installFromLocal) {
|
|
1727
|
+
// Use existing local upload behavior
|
|
1728
|
+
console.log(chalk_1.default.gray('Uploading agent code from local directory...'));
|
|
1729
|
+
const agentV2Path = path.resolve(__dirname, '../../../agent-v2');
|
|
1730
|
+
const agentDistPath = path.join(agentV2Path, 'dist');
|
|
1731
|
+
const agentPackageJsonPath = path.join(agentV2Path, 'package.json');
|
|
1732
|
+
if (!fs.existsSync(agentDistPath)) {
|
|
1733
|
+
throw new Error('Agent distribution not found. This feature requires development environment setup. Please use S3 download instead.');
|
|
1734
|
+
}
|
|
1735
|
+
if (!fs.existsSync(agentPackageJsonPath)) {
|
|
1736
|
+
throw new Error('Agent package.json not found.');
|
|
1737
|
+
}
|
|
1738
|
+
// Create a tarball of the dist directory and package.json
|
|
1739
|
+
const { execSync } = require('child_process');
|
|
1740
|
+
const tarPath = `/tmp/agent-dist-${Date.now()}.tar.gz`;
|
|
1741
|
+
try {
|
|
1742
|
+
console.log(chalk_1.default.gray('Creating agent distribution tarball...'));
|
|
1743
|
+
console.log(chalk_1.default.gray(` Source dist: ${agentDistPath}`));
|
|
1744
|
+
console.log(chalk_1.default.gray(` Source package.json: ${agentPackageJsonPath}`));
|
|
1745
|
+
// Create tarball with both dist and package.json
|
|
1746
|
+
const tempDir = `/tmp/agent-upload-${Date.now()}`;
|
|
1747
|
+
console.log(chalk_1.default.gray(` Creating temp directory: ${tempDir}`));
|
|
1748
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1749
|
+
fs.copyFileSync(agentPackageJsonPath, path.join(tempDir, 'package.json'));
|
|
1750
|
+
execSync(`cp -r "${agentDistPath}" "${tempDir}/dist"`, { stdio: 'inherit' });
|
|
1751
|
+
console.log(chalk_1.default.gray(` Creating tarball: ${tarPath}`));
|
|
1752
|
+
execSync(`cd "${tempDir}" && tar -czf "${tarPath}" .`, { stdio: 'inherit' });
|
|
1753
|
+
// Verify tarball was created
|
|
1754
|
+
if (!fs.existsSync(tarPath)) {
|
|
1755
|
+
throw new Error(`Failed to create tarball at ${tarPath}`);
|
|
1756
|
+
}
|
|
1757
|
+
const tarStats = fs.statSync(tarPath);
|
|
1758
|
+
const tarSizeMB = (tarStats.size / (1024 * 1024)).toFixed(2);
|
|
1759
|
+
console.log(chalk_1.default.gray(` Tarball created: ${tarSizeMB} MB`));
|
|
1760
|
+
// Clean up temp directory
|
|
1761
|
+
console.log(chalk_1.default.gray(' Cleaning up temp directory...'));
|
|
1762
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1763
|
+
// Upload tarball to user's home directory first (where we have write permissions)
|
|
1764
|
+
// Then move to /tmp with sudo if needed
|
|
1765
|
+
// Use explicit home directory path since SFTP might not expand ~
|
|
1766
|
+
const homeDir = connection.username === 'ec2-user' ? '/home/ec2-user' : `/home/${connection.username}`;
|
|
1767
|
+
const remoteHomePath = `${homeDir}/agent-dist-${Date.now()}.tar.gz`;
|
|
1768
|
+
const remoteTmpPath = `/tmp/agent-dist.tar.gz`;
|
|
1769
|
+
console.log(chalk_1.default.gray('Checking remote directory permissions...'));
|
|
1770
|
+
try {
|
|
1771
|
+
const homeDirCheck = await awsService.executeSSHCommand(connection, `test -d ${homeDir} && test -w ${homeDir} && echo "OK" || echo "FAIL"`);
|
|
1772
|
+
if (homeDirCheck.stdout.trim() !== 'OK') {
|
|
1773
|
+
console.log(chalk_1.default.yellow(` Warning: Home directory ${homeDir} may not be writable`));
|
|
1774
|
+
console.log(chalk_1.default.gray(` Attempting to verify with ls -ld: ${homeDir}`));
|
|
1775
|
+
const lsResult = await awsService.executeSSHCommand(connection, `ls -ld ${homeDir} 2>&1 || echo "directory_not_found"`);
|
|
1776
|
+
console.log(chalk_1.default.gray(` Directory info: ${lsResult.stdout.trim()}`));
|
|
1777
|
+
}
|
|
1778
|
+
else {
|
|
1779
|
+
console.log(chalk_1.default.gray(` ✓ Home directory ${homeDir} is writable`));
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
catch (checkError) {
|
|
1783
|
+
console.log(chalk_1.default.yellow(` Warning: Could not verify directory permissions: ${checkError instanceof Error ? checkError.message : String(checkError)}`));
|
|
1784
|
+
}
|
|
1785
|
+
console.log(chalk_1.default.blue('Uploading tarball to remote server...'));
|
|
1786
|
+
console.log(chalk_1.default.gray(` Local file: ${tarPath}`));
|
|
1787
|
+
console.log(chalk_1.default.gray(` Remote path: ${remoteHomePath}`));
|
|
1788
|
+
console.log(chalk_1.default.gray(` File size: ${tarSizeMB} MB`));
|
|
1789
|
+
try {
|
|
1790
|
+
await awsService.uploadFile(connection, tarPath, remoteHomePath);
|
|
1791
|
+
console.log(chalk_1.default.green(' ✓ Upload completed successfully'));
|
|
1792
|
+
// Verify file was uploaded
|
|
1793
|
+
console.log(chalk_1.default.gray(' Verifying uploaded file...'));
|
|
1794
|
+
const verifyResult = await awsService.executeSSHCommand(connection, `test -f ${remoteHomePath} && ls -lh ${remoteHomePath} || echo "file_not_found"`);
|
|
1795
|
+
if (verifyResult.stdout.includes('file_not_found')) {
|
|
1796
|
+
throw new Error(`Upload verification failed: file not found at ${remoteHomePath}`);
|
|
1797
|
+
}
|
|
1798
|
+
console.log(chalk_1.default.gray(` File verified: ${verifyResult.stdout.trim()}`));
|
|
1799
|
+
}
|
|
1800
|
+
catch (uploadError) {
|
|
1801
|
+
console.error(chalk_1.default.red(` ✗ Upload failed: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`));
|
|
1802
|
+
throw new Error(`Failed to upload file to ${remoteHomePath}: ${uploadError instanceof Error ? uploadError.message : String(uploadError)}`);
|
|
1803
|
+
}
|
|
1804
|
+
// Move to /tmp using sudo (in case /tmp has permission restrictions)
|
|
1805
|
+
console.log(chalk_1.default.gray(`Moving file from ${remoteHomePath} to ${remoteTmpPath}...`));
|
|
1806
|
+
try {
|
|
1807
|
+
const moveResult = await awsService.executeSSHCommand(connection, `sudo mv ${remoteHomePath} ${remoteTmpPath} && sudo chmod 644 ${remoteTmpPath} && echo "OK" || echo "FAIL"`);
|
|
1808
|
+
if (moveResult.stdout.trim() === 'OK') {
|
|
1809
|
+
console.log(chalk_1.default.green(' ✓ File moved successfully'));
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
throw new Error(`Failed to move file: ${moveResult.stderr || moveResult.stdout}`);
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
catch (moveError) {
|
|
1816
|
+
console.error(chalk_1.default.red(` ✗ Move failed: ${moveError instanceof Error ? moveError.message : String(moveError)}`));
|
|
1817
|
+
throw new Error(`Failed to move file from ${remoteHomePath} to ${remoteTmpPath}: ${moveError instanceof Error ? moveError.message : String(moveError)}`);
|
|
1818
|
+
}
|
|
1819
|
+
// Extract on remote server
|
|
1820
|
+
console.log(chalk_1.default.gray('Extracting agent code...'));
|
|
1821
|
+
try {
|
|
1822
|
+
const extractResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo tar -xzf /tmp/agent-dist.tar.gz && sudo rm /tmp/agent-dist.tar.gz && echo "OK" || echo "FAIL"');
|
|
1823
|
+
if (extractResult.stdout.trim() === 'OK') {
|
|
1824
|
+
console.log(chalk_1.default.green(' ✓ Extraction completed successfully'));
|
|
1825
|
+
}
|
|
1826
|
+
else {
|
|
1827
|
+
console.log(chalk_1.default.yellow(` Warning: Extraction may have failed: ${extractResult.stderr || extractResult.stdout}`));
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
catch (extractError) {
|
|
1831
|
+
console.error(chalk_1.default.red(` ✗ Extraction failed: ${extractError instanceof Error ? extractError.message : String(extractError)}`));
|
|
1832
|
+
throw new Error(`Failed to extract agent code: ${extractError instanceof Error ? extractError.message : String(extractError)}`);
|
|
1833
|
+
}
|
|
1834
|
+
// Install production dependencies (run as root since directory is owned by root)
|
|
1835
|
+
console.log(chalk_1.default.gray('Installing agent dependencies...'));
|
|
1836
|
+
try {
|
|
1837
|
+
const installResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo npm install --production --no-save');
|
|
1838
|
+
if (installResult.exitCode === 0) {
|
|
1839
|
+
console.log(chalk_1.default.green(' ✓ Dependencies installed successfully'));
|
|
1840
|
+
}
|
|
1841
|
+
else {
|
|
1842
|
+
console.log(chalk_1.default.yellow(` Warning: npm install exited with code ${installResult.exitCode}`));
|
|
1843
|
+
console.log(chalk_1.default.gray(` stdout: ${installResult.stdout}`));
|
|
1844
|
+
if (installResult.stderr) {
|
|
1845
|
+
console.log(chalk_1.default.gray(` stderr: ${installResult.stderr}`));
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
catch (installError) {
|
|
1850
|
+
console.error(chalk_1.default.red(` ✗ Dependency installation failed: ${installError instanceof Error ? installError.message : String(installError)}`));
|
|
1851
|
+
throw new Error(`Failed to install dependencies: ${installError instanceof Error ? installError.message : String(installError)}`);
|
|
1852
|
+
}
|
|
1853
|
+
// Ensure index.js is executable
|
|
1854
|
+
console.log(chalk_1.default.gray('Setting file permissions...'));
|
|
1855
|
+
try {
|
|
1856
|
+
await awsService.executeSSHCommand(connection, 'sudo chmod +x /opt/edgible-agent/dist/index.js || true');
|
|
1857
|
+
// Set proper ownership (root will own everything)
|
|
1858
|
+
await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent');
|
|
1859
|
+
// Create logs directory for root user
|
|
1860
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /opt/edgible-agent/.edgible/agent/logs');
|
|
1861
|
+
await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent/.edgible');
|
|
1862
|
+
await awsService.executeSSHCommand(connection, 'sudo chmod -R 755 /opt/edgible-agent/.edgible');
|
|
1863
|
+
console.log(chalk_1.default.green(' ✓ Permissions and ownership set correctly'));
|
|
1864
|
+
}
|
|
1865
|
+
catch (permError) {
|
|
1866
|
+
console.error(chalk_1.default.red(` ✗ Failed to set permissions: ${permError instanceof Error ? permError.message : String(permError)}`));
|
|
1867
|
+
throw new Error(`Failed to set file permissions: ${permError instanceof Error ? permError.message : String(permError)}`);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
finally {
|
|
1871
|
+
// Clean up tarball if it exists
|
|
1872
|
+
if (fs.existsSync(tarPath)) {
|
|
1873
|
+
fs.unlinkSync(tarPath);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
else {
|
|
1878
|
+
// Download from CloudFront on remote server
|
|
1879
|
+
console.log(chalk_1.default.gray('Downloading agent from CloudFront...'));
|
|
1880
|
+
const distributionUrl = (0, urls_1.getDistributionUrl)();
|
|
1881
|
+
const version = agentVersion || 'latest';
|
|
1882
|
+
// Build CloudFront URL directly - no bucket or S3 path needed
|
|
1883
|
+
// Stage determines both the distribution URL and the bucket (backend detail)
|
|
1884
|
+
const downloadUrl = `${distributionUrl}/${version}.zip`;
|
|
1885
|
+
console.log(chalk_1.default.gray(` Distribution: ${distributionUrl}`));
|
|
1886
|
+
console.log(chalk_1.default.gray(` Version: ${version}`));
|
|
1887
|
+
console.log(chalk_1.default.gray(` URL: ${downloadUrl}`));
|
|
1888
|
+
// Check if unzip is installed, install if needed
|
|
1889
|
+
console.log(chalk_1.default.gray('Checking for unzip utility...'));
|
|
1890
|
+
const unzipCheck = await awsService.executeSSHCommand(connection, 'which unzip || echo "unzip not found"');
|
|
1891
|
+
if (unzipCheck.stdout.trim() === 'unzip not found') {
|
|
1892
|
+
console.log(chalk_1.default.gray('Installing unzip...'));
|
|
1893
|
+
await awsService.executeSSHCommand(connection, 'sudo yum install -y unzip || sudo apt-get install -y unzip');
|
|
1894
|
+
}
|
|
1895
|
+
else {
|
|
1896
|
+
console.log(chalk_1.default.gray('✓ unzip is installed'));
|
|
1897
|
+
}
|
|
1898
|
+
// Download and extract on remote server with verbose output
|
|
1899
|
+
const downloadScript = `
|
|
1900
|
+
set -e
|
|
1901
|
+
echo "Step 1: Cleaning old files..."
|
|
1902
|
+
cd /opt/edgible-agent && sudo rm -rf dist package.json node_modules index.js *.map && echo "✓ Cleaned"
|
|
1903
|
+
echo "Step 2: Downloading agent..."
|
|
1904
|
+
sudo curl -L -f --max-time 300 --connect-timeout 30 -w "Downloaded %{size_download} bytes in %{time_total}s\\n" -o /tmp/agent.zip "${downloadUrl}" && echo "✓ Downloaded"
|
|
1905
|
+
echo "Step 3: Extracting agent..."
|
|
1906
|
+
sudo unzip -o -q /tmp/agent.zip -d /opt/edgible-agent && echo "✓ Extracted"
|
|
1907
|
+
echo "Step 4: Cleaning up..."
|
|
1908
|
+
sudo rm /tmp/agent.zip && echo "✓ Cleanup complete"
|
|
1909
|
+
echo "OK"
|
|
1910
|
+
`;
|
|
1911
|
+
console.log(chalk_1.default.gray(' Downloading agent (this may take a moment)...'));
|
|
1912
|
+
try {
|
|
1913
|
+
const downloadResult = await awsService.executeSSHCommand(connection, downloadScript);
|
|
1914
|
+
// Log the output for debugging
|
|
1915
|
+
if (downloadResult.stdout) {
|
|
1916
|
+
console.log(chalk_1.default.gray(' Output:'), downloadResult.stdout.trim());
|
|
1917
|
+
}
|
|
1918
|
+
if (downloadResult.stderr) {
|
|
1919
|
+
console.log(chalk_1.default.yellow(' Stderr:'), downloadResult.stderr.trim());
|
|
1920
|
+
}
|
|
1921
|
+
if (downloadResult.stdout.includes('OK')) {
|
|
1922
|
+
console.log(chalk_1.default.green(' ✓ Download and extraction completed successfully'));
|
|
1923
|
+
}
|
|
1924
|
+
else {
|
|
1925
|
+
throw new Error(`Download failed: ${downloadResult.stderr || downloadResult.stdout}`);
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
catch (downloadError) {
|
|
1929
|
+
console.error(chalk_1.default.red(` ✗ Download failed: ${downloadError instanceof Error ? downloadError.message : String(downloadError)}`));
|
|
1930
|
+
throw downloadError;
|
|
1931
|
+
}
|
|
1932
|
+
// Verify dependencies are present (already included in zip)
|
|
1933
|
+
console.log(chalk_1.default.gray('Verifying agent dependencies...'));
|
|
1934
|
+
try {
|
|
1935
|
+
const verifyResult = await awsService.executeSSHCommand(connection, 'test -d /opt/edgible-agent/node_modules/commander && echo "OK" || echo "MISSING"');
|
|
1936
|
+
if (verifyResult.stdout.trim() === 'OK') {
|
|
1937
|
+
console.log(chalk_1.default.green(' ✓ Dependencies verified (included in package)'));
|
|
1938
|
+
}
|
|
1939
|
+
else {
|
|
1940
|
+
console.log(chalk_1.default.yellow(' ⚠ Dependencies missing, installing...'));
|
|
1941
|
+
const installResult = await awsService.executeSSHCommand(connection, 'cd /opt/edgible-agent && sudo npm install --production --no-save --no-audit --no-fund --loglevel=error');
|
|
1942
|
+
if (installResult.exitCode === 0) {
|
|
1943
|
+
console.log(chalk_1.default.green(' ✓ Dependencies installed successfully'));
|
|
1944
|
+
}
|
|
1945
|
+
else {
|
|
1946
|
+
console.log(chalk_1.default.yellow(` Warning: npm install exited with code ${installResult.exitCode}`));
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
catch (verifyError) {
|
|
1951
|
+
console.log(chalk_1.default.yellow(` ⚠ Could not verify dependencies: ${verifyError instanceof Error ? verifyError.message : String(verifyError)}`));
|
|
1952
|
+
}
|
|
1953
|
+
// Set file permissions
|
|
1954
|
+
console.log(chalk_1.default.gray('Setting file permissions...'));
|
|
1955
|
+
try {
|
|
1956
|
+
await awsService.executeSSHCommand(connection, 'sudo chmod +x /opt/edgible-agent/index.js || true');
|
|
1957
|
+
await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent');
|
|
1958
|
+
await awsService.executeSSHCommand(connection, 'sudo mkdir -p /opt/edgible-agent/.edgible/agent/logs');
|
|
1959
|
+
await awsService.executeSSHCommand(connection, 'sudo chown -R root:root /opt/edgible-agent/.edgible');
|
|
1960
|
+
await awsService.executeSSHCommand(connection, 'sudo chmod -R 755 /opt/edgible-agent/.edgible');
|
|
1961
|
+
console.log(chalk_1.default.green(' ✓ Permissions and ownership set correctly'));
|
|
1962
|
+
}
|
|
1963
|
+
catch (permError) {
|
|
1964
|
+
console.error(chalk_1.default.red(` ✗ Failed to set permissions: ${permError instanceof Error ? permError.message : String(permError)}`));
|
|
1965
|
+
throw new Error(`Failed to set file permissions: ${permError instanceof Error ? permError.message : String(permError)}`);
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
// Start agent service
|
|
1969
|
+
console.log(chalk_1.default.gray('Starting agent service...'));
|
|
1970
|
+
try {
|
|
1971
|
+
await awsService.executeSSHCommand(connection, 'sudo systemctl restart edgible-agent');
|
|
1972
|
+
}
|
|
1973
|
+
catch (startError) {
|
|
1974
|
+
console.error(chalk_1.default.red('Failed to start service:'), startError);
|
|
1975
|
+
// Check service status and logs
|
|
1976
|
+
const statusResult = await awsService.executeSSHCommand(connection, 'sudo systemctl status edgible-agent');
|
|
1977
|
+
console.log(chalk_1.default.red('Service status:'), statusResult.stdout);
|
|
1978
|
+
console.log(chalk_1.default.red('Service stderr:'), statusResult.stderr);
|
|
1979
|
+
throw new Error('Agent service failed to start');
|
|
1980
|
+
}
|
|
1981
|
+
// Verify agent is running
|
|
1982
|
+
const statusResult = await awsService.executeSSHCommand(connection, 'sudo systemctl is-active edgible-agent');
|
|
1983
|
+
if (statusResult.stdout.trim() !== 'active') {
|
|
1984
|
+
// Get more details about why it failed
|
|
1985
|
+
const statusDetails = await awsService.executeSSHCommand(connection, 'sudo systemctl status edgible-agent');
|
|
1986
|
+
console.log(chalk_1.default.red('Service status details:'), statusDetails.stdout);
|
|
1987
|
+
throw new Error('Agent service failed to start');
|
|
1988
|
+
}
|
|
1989
|
+
console.log(chalk_1.default.green('✓ Agent resynced successfully!'));
|
|
1990
|
+
return {
|
|
1991
|
+
message: 'Agent resynced successfully',
|
|
1992
|
+
success: true,
|
|
1993
|
+
agentVersion: agentVersion || 'latest',
|
|
1994
|
+
syncTimestamp: new Date().toISOString()
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
catch (error) {
|
|
1998
|
+
console.error(chalk_1.default.red('Error resyncing agent:'), error);
|
|
1999
|
+
throw error;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Get comprehensive gateway diagnostics including status, logs, and system info
|
|
2004
|
+
*/
|
|
2005
|
+
async getGatewayDiagnostics(gatewayId) {
|
|
2006
|
+
try {
|
|
2007
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
2008
|
+
await this.attemptAutoRelogin();
|
|
2009
|
+
const configManager = new config_1.ConfigManager();
|
|
2010
|
+
const gatewayInfo = configManager.getGateway(gatewayId);
|
|
2011
|
+
if (!gatewayInfo) {
|
|
2012
|
+
throw new Error('Gateway not found in local configuration');
|
|
2013
|
+
}
|
|
2014
|
+
// Load SSH key
|
|
2015
|
+
const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
|
|
2016
|
+
const privateKey = awsService.loadSSHKey(gatewayInfo.keyPath);
|
|
2017
|
+
// Wait for instance to be ready for SSH
|
|
2018
|
+
const isReady = await awsService.waitForSSHReady(gatewayInfo.ec2InstanceId);
|
|
2019
|
+
if (!isReady) {
|
|
2020
|
+
throw new Error('Instance is not ready for SSH connections');
|
|
2021
|
+
}
|
|
2022
|
+
// Create SSH connection
|
|
2023
|
+
const connection = {
|
|
2024
|
+
host: gatewayInfo.publicIp,
|
|
2025
|
+
port: 22,
|
|
2026
|
+
username: 'ec2-user',
|
|
2027
|
+
privateKey: privateKey
|
|
2028
|
+
};
|
|
2029
|
+
// Execute multiple commands in parallel where possible
|
|
2030
|
+
// For logs, try multiple locations and fall back to journalctl
|
|
2031
|
+
const commands = {
|
|
2032
|
+
status: 'cat ~/.edgible/agent/status.json /opt/edgible-agent/.edgible/agent/status.json 2>/dev/null | head -1 || echo "Status file not found"',
|
|
2033
|
+
agentLogs: '(test -f ~/.edgible/agent/logs/agent.log && tail -n 100 ~/.edgible/agent/logs/agent.log) || (test -f /opt/edgible-agent/.edgible/agent/logs/agent.log && tail -n 100 /opt/edgible-agent/.edgible/agent/logs/agent.log) || (sudo journalctl -u edgible-agent.service -n 100 --no-pager 2>/dev/null) || echo "No logs available (checking journalctl...)"',
|
|
2034
|
+
systemInfo: 'echo "=== System Resources ===" && free -h && echo "" && echo "=== Disk Usage ===" && df -h / && echo "" && echo "=== Uptime ===" && uptime && echo "" && echo "=== Load Average ===" && cat /proc/loadavg',
|
|
2035
|
+
wireguardStatus: 'sudo wg show 2>/dev/null || echo "WireGuard not running or no interfaces"',
|
|
2036
|
+
caddyStatus: 'curl -s http://localhost:2019/config/ 2>/dev/null | head -50 || echo "Caddy API not accessible"',
|
|
2037
|
+
serviceStatus: 'sudo systemctl status edgible-agent.service --no-pager -l 2>/dev/null || echo "Service status unavailable"',
|
|
2038
|
+
networkInfo: 'echo "=== Network Interfaces ===" && ip addr show && echo "" && echo "=== WireGuard Interfaces ===" && ip link show type wireguard 2>/dev/null || echo "No WireGuard interfaces"',
|
|
2039
|
+
logPaths: 'echo "=== Checking log paths ===" && (test -d ~/.edgible/agent/logs && echo "~/.edgible/agent/logs exists" && ls -la ~/.edgible/agent/logs/ 2>/dev/null) || echo "~/.edgible/agent/logs not found" && (test -d /opt/edgible-agent/.edgible/agent/logs && echo "/opt/edgible-agent/.edgible/agent/logs exists" && ls -la /opt/edgible-agent/.edgible/agent/logs/ 2>/dev/null) || echo "/opt/edgible-agent/.edgible/agent/logs not found"'
|
|
2040
|
+
};
|
|
2041
|
+
const results = {};
|
|
2042
|
+
// Execute all commands
|
|
2043
|
+
for (const [key, command] of Object.entries(commands)) {
|
|
2044
|
+
try {
|
|
2045
|
+
const result = await awsService.executeSSHCommand(connection, command);
|
|
2046
|
+
results[key] = result.stdout || result.stderr || '';
|
|
2047
|
+
}
|
|
2048
|
+
catch (error) {
|
|
2049
|
+
results[key] = `Error executing command: ${error instanceof Error ? error.message : String(error)}`;
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
return {
|
|
2053
|
+
status: results.status,
|
|
2054
|
+
agentLogs: results.agentLogs,
|
|
2055
|
+
systemInfo: results.systemInfo,
|
|
2056
|
+
wireguardStatus: results.wireguardStatus,
|
|
2057
|
+
caddyStatus: results.caddyStatus,
|
|
2058
|
+
serviceStatus: results.serviceStatus,
|
|
2059
|
+
networkInfo: results.networkInfo,
|
|
2060
|
+
logPaths: results.logPaths || '',
|
|
2061
|
+
success: true
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
catch (error) {
|
|
2065
|
+
console.error(chalk_1.default.red('Error retrieving gateway diagnostics:'), error);
|
|
2066
|
+
throw error;
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
/**
|
|
2070
|
+
* Get agent logs from a gateway
|
|
2071
|
+
*/
|
|
2072
|
+
/**
|
|
2073
|
+
* Get logs from a managed gateway (admin only)
|
|
2074
|
+
*/
|
|
2075
|
+
async getManagedGatewayLogs(gatewayId, options) {
|
|
2076
|
+
try {
|
|
2077
|
+
await this.attemptAutoRelogin();
|
|
2078
|
+
// Get managed gateway details
|
|
2079
|
+
const gatewayDetails = await this.getManagedGateway(gatewayId);
|
|
2080
|
+
if (!gatewayDetails.success || !gatewayDetails.gateway) {
|
|
2081
|
+
throw new Error('Managed gateway not found');
|
|
2082
|
+
}
|
|
2083
|
+
const gateway = gatewayDetails.gateway;
|
|
2084
|
+
const ipAddress = gateway.ipAddress;
|
|
2085
|
+
if (!ipAddress) {
|
|
2086
|
+
throw new Error('Gateway IP address not available');
|
|
2087
|
+
}
|
|
2088
|
+
// Get SSH key from backend
|
|
2089
|
+
const sshKeyResponse = await this.apiClient.getManagedGatewaySSHKey({ gatewayId });
|
|
2090
|
+
if (!sshKeyResponse.success || !sshKeyResponse.sshPrivateKey) {
|
|
2091
|
+
throw new Error('Failed to retrieve SSH key for managed gateway');
|
|
2092
|
+
}
|
|
2093
|
+
const sshPrivateKey = sshKeyResponse.sshPrivateKey;
|
|
2094
|
+
// Create SSH connection
|
|
2095
|
+
const connection = {
|
|
2096
|
+
host: ipAddress,
|
|
2097
|
+
port: 22,
|
|
2098
|
+
username: 'ec2-user',
|
|
2099
|
+
privateKey: sshPrivateKey
|
|
2100
|
+
};
|
|
2101
|
+
// Use the same logic as getGatewayLogs but with custom connection
|
|
2102
|
+
return await this.executeGatewayLogs(connection, options);
|
|
2103
|
+
}
|
|
2104
|
+
catch (error) {
|
|
2105
|
+
console.error(chalk_1.default.red('Error getting managed gateway logs:'), error);
|
|
2106
|
+
throw error;
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Execute a command on a managed gateway
|
|
2111
|
+
*/
|
|
2112
|
+
async executeManagedGatewayCommand(gatewayId, command) {
|
|
2113
|
+
try {
|
|
2114
|
+
await this.attemptAutoRelogin();
|
|
2115
|
+
// Get managed gateway details
|
|
2116
|
+
const gatewayDetails = await this.getManagedGateway(gatewayId);
|
|
2117
|
+
if (!gatewayDetails.success || !gatewayDetails.gateway) {
|
|
2118
|
+
throw new Error('Managed gateway not found');
|
|
2119
|
+
}
|
|
2120
|
+
const gateway = gatewayDetails.gateway;
|
|
2121
|
+
const ipAddress = gateway.ipAddress;
|
|
2122
|
+
if (!ipAddress) {
|
|
2123
|
+
throw new Error('Gateway IP address not available');
|
|
2124
|
+
}
|
|
2125
|
+
// Get SSH key from backend
|
|
2126
|
+
const sshKeyResponse = await this.apiClient.getManagedGatewaySSHKey({ gatewayId });
|
|
2127
|
+
if (!sshKeyResponse.success || !sshKeyResponse.sshPrivateKey) {
|
|
2128
|
+
throw new Error('Failed to retrieve SSH key for managed gateway');
|
|
2129
|
+
}
|
|
2130
|
+
const sshPrivateKey = sshKeyResponse.sshPrivateKey;
|
|
2131
|
+
// Create SSH connection
|
|
2132
|
+
const connection = {
|
|
2133
|
+
host: ipAddress,
|
|
2134
|
+
port: 22,
|
|
2135
|
+
username: 'ec2-user',
|
|
2136
|
+
privateKey: sshPrivateKey
|
|
2137
|
+
};
|
|
2138
|
+
const { AWSService } = await Promise.resolve().then(() => __importStar(require('./aws')));
|
|
2139
|
+
const awsService = new AWSService();
|
|
2140
|
+
// Test SSH connection
|
|
2141
|
+
try {
|
|
2142
|
+
await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
|
|
2143
|
+
}
|
|
2144
|
+
catch (sshError) {
|
|
2145
|
+
throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
|
|
2146
|
+
}
|
|
2147
|
+
// Execute the command
|
|
2148
|
+
const result = await awsService.executeSSHCommand(connection, command);
|
|
2149
|
+
return {
|
|
2150
|
+
success: result.exitCode === 0,
|
|
2151
|
+
output: result.stdout,
|
|
2152
|
+
error: result.stderr || undefined
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
catch (error) {
|
|
2156
|
+
return {
|
|
2157
|
+
success: false,
|
|
2158
|
+
output: '',
|
|
2159
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
/**
|
|
2164
|
+
* Execute gateway logs retrieval (internal method)
|
|
2165
|
+
*/
|
|
2166
|
+
async executeGatewayLogs(connection, options) {
|
|
2167
|
+
const { AWSService } = await Promise.resolve().then(() => __importStar(require('./aws')));
|
|
2168
|
+
const awsService = new AWSService();
|
|
2169
|
+
// Test SSH connection
|
|
2170
|
+
try {
|
|
2171
|
+
await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
|
|
2172
|
+
}
|
|
2173
|
+
catch (sshError) {
|
|
2174
|
+
throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
|
|
2175
|
+
}
|
|
2176
|
+
const lines = options.lines || 50;
|
|
2177
|
+
const follow = options.follow || false;
|
|
2178
|
+
const level = options.level || 'all';
|
|
2179
|
+
const comprehensive = options.comprehensive || false;
|
|
2180
|
+
if (comprehensive) {
|
|
2181
|
+
// Comprehensive diagnostics
|
|
2182
|
+
const commands = [
|
|
2183
|
+
'systemctl status edgible-agent --no-pager || echo "Service not found"',
|
|
2184
|
+
'hostname && uname -a',
|
|
2185
|
+
'wg show || echo "WireGuard not configured"',
|
|
2186
|
+
'haproxy -v 2>&1 || echo "HAProxy not found"',
|
|
2187
|
+
'iptables -L -n -v | head -20 || echo "iptables not available"',
|
|
2188
|
+
'df -h',
|
|
2189
|
+
'free -h',
|
|
2190
|
+
'uptime'
|
|
2191
|
+
];
|
|
2192
|
+
let output = '';
|
|
2193
|
+
for (const cmd of commands) {
|
|
2194
|
+
try {
|
|
2195
|
+
const result = await awsService.executeSSHCommand(connection, cmd);
|
|
2196
|
+
output += `\n=== ${cmd} ===\n${result.stdout}\n`;
|
|
2197
|
+
}
|
|
2198
|
+
catch (error) {
|
|
2199
|
+
output += `\n=== ${cmd} ===\nError: ${error instanceof Error ? error.message : String(error)}\n`;
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
return { success: true, logs: output };
|
|
2203
|
+
}
|
|
2204
|
+
// Regular logs
|
|
2205
|
+
const journalCommand = 'journalctl -u edgible-agent --no-pager';
|
|
2206
|
+
// Map log levels to journalctl priority levels
|
|
2207
|
+
// journalctl -p uses syslog priorities: emerg(0), alert(1), crit(2), err(3), warning(4), notice(5), info(6), debug(7)
|
|
2208
|
+
// Using -p shows that priority and above, so -p debug shows all logs including debug
|
|
2209
|
+
let levelFilter = '';
|
|
2210
|
+
if (level !== 'all') {
|
|
2211
|
+
// Map our level names to journalctl priority names
|
|
2212
|
+
const priorityMap = {
|
|
2213
|
+
'error': 'err',
|
|
2214
|
+
'warn': 'warning',
|
|
2215
|
+
'info': 'info',
|
|
2216
|
+
'debug': 'debug'
|
|
2217
|
+
};
|
|
2218
|
+
const journalPriority = priorityMap[level] || level;
|
|
2219
|
+
levelFilter = ` -p ${journalPriority}`;
|
|
2220
|
+
}
|
|
2221
|
+
else {
|
|
2222
|
+
// When level is 'all', explicitly include all priorities including debug
|
|
2223
|
+
// Some systems may filter debug logs by default, so we explicitly include them
|
|
2224
|
+
levelFilter = ' --priority=0..7';
|
|
2225
|
+
}
|
|
2226
|
+
const linesArg = follow ? '' : ` -n ${lines}`;
|
|
2227
|
+
if (follow) {
|
|
2228
|
+
// Follow logs in real-time
|
|
2229
|
+
const result = await awsService.executeSSHCommand(connection, journalCommand + levelFilter + ' -f');
|
|
2230
|
+
return { success: true, logs: result.stdout };
|
|
2231
|
+
}
|
|
2232
|
+
else {
|
|
2233
|
+
const result = await awsService.executeSSHCommand(connection, journalCommand + levelFilter + linesArg);
|
|
2234
|
+
return { success: true, logs: result.stdout };
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
async getGatewayLogs(gatewayId, options = {}) {
|
|
2238
|
+
try {
|
|
2239
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
2240
|
+
await this.attemptAutoRelogin();
|
|
2241
|
+
const configManager = new config_1.ConfigManager();
|
|
2242
|
+
const gatewayInfo = configManager.getGateway(gatewayId);
|
|
2243
|
+
if (!gatewayInfo) {
|
|
2244
|
+
throw new Error('Gateway not found in local configuration');
|
|
2245
|
+
}
|
|
2246
|
+
// Load SSH key
|
|
2247
|
+
const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
|
|
2248
|
+
const privateKey = awsService.loadSSHKey(gatewayInfo.keyPath);
|
|
2249
|
+
// Wait for instance to be ready for SSH
|
|
2250
|
+
const isReady = await awsService.waitForSSHReady(gatewayInfo.ec2InstanceId);
|
|
2251
|
+
if (!isReady) {
|
|
2252
|
+
throw new Error('Instance is not ready for SSH connections');
|
|
2253
|
+
}
|
|
2254
|
+
// If comprehensive mode, return diagnostics (this handles SSH connection internally)
|
|
2255
|
+
if (options.comprehensive) {
|
|
2256
|
+
const diagnostics = await this.getGatewayDiagnostics(gatewayId);
|
|
2257
|
+
let output = chalk_1.default.blue.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
2258
|
+
output += chalk_1.default.blue.bold(' GATEWAY DIAGNOSTICS\n');
|
|
2259
|
+
output += chalk_1.default.blue.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n');
|
|
2260
|
+
// Agent Status
|
|
2261
|
+
output += chalk_1.default.yellow.bold('📊 AGENT STATUS\n');
|
|
2262
|
+
output += chalk_1.default.gray('─'.repeat(80) + '\n');
|
|
2263
|
+
try {
|
|
2264
|
+
const status = JSON.parse(diagnostics.status);
|
|
2265
|
+
output += chalk_1.default.white(`Running: ${status.running ? chalk_1.default.green('✓ Yes') : chalk_1.default.red('✗ No')}\n`);
|
|
2266
|
+
output += chalk_1.default.white(`Health: ${status.health || 'unknown'}\n`);
|
|
2267
|
+
output += chalk_1.default.white(`Device ID: ${status.deviceId || 'N/A'}\n`);
|
|
2268
|
+
output += chalk_1.default.white(`Device Type: ${status.deviceType || 'N/A'}\n`);
|
|
2269
|
+
output += chalk_1.default.white(`Uptime: ${status.uptime ? Math.round(status.uptime / 1000 / 60) + ' minutes' : 'N/A'}\n`);
|
|
2270
|
+
output += chalk_1.default.white(`Last Poll: ${status.lastPoll || 'N/A'}\n`);
|
|
2271
|
+
output += chalk_1.default.white(`API Connected: ${status.apiConnected ? chalk_1.default.green('✓ Yes') : chalk_1.default.red('✗ No')}\n`);
|
|
2272
|
+
output += chalk_1.default.white(`Applications: ${status.applications?.length || 0}\n`);
|
|
2273
|
+
if (status.lastError) {
|
|
2274
|
+
output += chalk_1.default.red(`Last Error: ${status.lastError}\n`);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
catch {
|
|
2278
|
+
output += chalk_1.default.gray(diagnostics.status + '\n');
|
|
2279
|
+
}
|
|
2280
|
+
output += '\n';
|
|
2281
|
+
// System Information
|
|
2282
|
+
output += chalk_1.default.yellow.bold('🖥️ SYSTEM INFORMATION\n');
|
|
2283
|
+
output += chalk_1.default.gray('─'.repeat(80) + '\n');
|
|
2284
|
+
output += chalk_1.default.white(diagnostics.systemInfo + '\n');
|
|
2285
|
+
output += '\n';
|
|
2286
|
+
// Service Status
|
|
2287
|
+
output += chalk_1.default.yellow.bold('⚙️ SERVICE STATUS\n');
|
|
2288
|
+
output += chalk_1.default.gray('─'.repeat(80) + '\n');
|
|
2289
|
+
output += chalk_1.default.white(diagnostics.serviceStatus + '\n');
|
|
2290
|
+
output += '\n';
|
|
2291
|
+
// WireGuard Status
|
|
2292
|
+
output += chalk_1.default.yellow.bold('🔐 WIREGUARD STATUS\n');
|
|
2293
|
+
output += chalk_1.default.gray('─'.repeat(80) + '\n');
|
|
2294
|
+
output += chalk_1.default.white(diagnostics.wireguardStatus + '\n');
|
|
2295
|
+
output += '\n';
|
|
2296
|
+
// Network Information
|
|
2297
|
+
output += chalk_1.default.yellow.bold('🌐 NETWORK INFORMATION\n');
|
|
2298
|
+
output += chalk_1.default.gray('─'.repeat(80) + '\n');
|
|
2299
|
+
output += chalk_1.default.white(diagnostics.networkInfo + '\n');
|
|
2300
|
+
output += '\n';
|
|
2301
|
+
// Caddy Status
|
|
2302
|
+
output += chalk_1.default.yellow.bold('🚀 CADDY STATUS\n');
|
|
2303
|
+
output += chalk_1.default.gray('─'.repeat(80) + '\n');
|
|
2304
|
+
output += chalk_1.default.white(diagnostics.caddyStatus + '\n');
|
|
2305
|
+
output += '\n';
|
|
2306
|
+
// Log Paths Info (if available)
|
|
2307
|
+
if (diagnostics.logPaths && diagnostics.logPaths.trim() && !diagnostics.logPaths.includes('Checking log paths')) {
|
|
2308
|
+
output += chalk_1.default.yellow.bold('📁 LOG FILE PATHS\n');
|
|
2309
|
+
output += chalk_1.default.gray('─'.repeat(80) + '\n');
|
|
2310
|
+
output += chalk_1.default.white(diagnostics.logPaths + '\n');
|
|
2311
|
+
output += '\n';
|
|
2312
|
+
}
|
|
2313
|
+
// Agent Logs
|
|
2314
|
+
output += chalk_1.default.yellow.bold('📋 AGENT LOGS (Last 100 lines)\n');
|
|
2315
|
+
output += chalk_1.default.gray('─'.repeat(80) + '\n');
|
|
2316
|
+
if (diagnostics.agentLogs && diagnostics.agentLogs.trim() &&
|
|
2317
|
+
!diagnostics.agentLogs.includes('No logs available') &&
|
|
2318
|
+
!diagnostics.agentLogs.includes('Log file not found')) {
|
|
2319
|
+
output += chalk_1.default.white(diagnostics.agentLogs + '\n');
|
|
2320
|
+
}
|
|
2321
|
+
else {
|
|
2322
|
+
// Logs not found in file, try journalctl
|
|
2323
|
+
output += chalk_1.default.yellow('⚠ Log file not found. Using journalctl (systemd logs):\n');
|
|
2324
|
+
output += chalk_1.default.gray(' (Logs may only be available via systemd journal)\n\n');
|
|
2325
|
+
// The agentLogs command already tries journalctl as fallback, so it should have the logs
|
|
2326
|
+
// But if it still failed, show what we got
|
|
2327
|
+
if (diagnostics.agentLogs && diagnostics.agentLogs.trim()) {
|
|
2328
|
+
output += chalk_1.default.white(diagnostics.agentLogs + '\n');
|
|
2329
|
+
}
|
|
2330
|
+
else {
|
|
2331
|
+
output += chalk_1.default.red('✗ Could not retrieve logs from any source\n');
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
output += '\n';
|
|
2335
|
+
output += chalk_1.default.blue.bold('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
2336
|
+
return {
|
|
2337
|
+
logs: output,
|
|
2338
|
+
success: true
|
|
2339
|
+
};
|
|
2340
|
+
}
|
|
2341
|
+
// Create SSH connection for regular log retrieval
|
|
2342
|
+
const connection = {
|
|
2343
|
+
host: gatewayInfo.publicIp,
|
|
2344
|
+
port: 22,
|
|
2345
|
+
username: 'ec2-user',
|
|
2346
|
+
privateKey: privateKey
|
|
2347
|
+
};
|
|
2348
|
+
console.log(chalk_1.default.blue('Connecting to gateway...'));
|
|
2349
|
+
console.log(chalk_1.default.gray(`SSH Host: ${connection.host}:${connection.port}`));
|
|
2350
|
+
// Test SSH connection first
|
|
2351
|
+
console.log(chalk_1.default.gray('Testing SSH connection...'));
|
|
2352
|
+
try {
|
|
2353
|
+
await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
|
|
2354
|
+
console.log(chalk_1.default.green('✓ SSH connection established'));
|
|
2355
|
+
}
|
|
2356
|
+
catch (sshError) {
|
|
2357
|
+
console.error(chalk_1.default.red('✗ SSH connection failed:'), sshError);
|
|
2358
|
+
throw new Error(`SSH connection failed: ${sshError instanceof Error ? sshError.message : 'Unknown error'}`);
|
|
2359
|
+
}
|
|
2360
|
+
// Build journalctl command
|
|
2361
|
+
let journalCommand = 'sudo journalctl -u edgible-agent.service --no-pager';
|
|
2362
|
+
if (options.lines) {
|
|
2363
|
+
journalCommand += ` -n ${options.lines}`;
|
|
2364
|
+
}
|
|
2365
|
+
if (options.level && options.level !== 'all') {
|
|
2366
|
+
journalCommand += ` --priority=${options.level}`;
|
|
2367
|
+
}
|
|
2368
|
+
console.log(chalk_1.default.gray('Retrieving agent logs...'));
|
|
2369
|
+
if (options.follow) {
|
|
2370
|
+
console.log(chalk_1.default.yellow('Following logs (press Ctrl+C to stop)...'));
|
|
2371
|
+
// For follow mode, we'll stream the logs
|
|
2372
|
+
const result = await awsService.executeSSHCommand(connection, journalCommand + ' -f');
|
|
2373
|
+
return {
|
|
2374
|
+
logs: result.stdout,
|
|
2375
|
+
success: true
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
else {
|
|
2379
|
+
// For regular mode, get the logs and return
|
|
2380
|
+
const result = await awsService.executeSSHCommand(connection, journalCommand);
|
|
2381
|
+
return {
|
|
2382
|
+
logs: result.stdout,
|
|
2383
|
+
success: true
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
catch (error) {
|
|
2388
|
+
console.error(chalk_1.default.red('Error retrieving gateway logs:'), error);
|
|
2389
|
+
throw error;
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
/**
|
|
2393
|
+
* Wipe agent log file on a gateway
|
|
2394
|
+
*/
|
|
2395
|
+
async wipeGatewayLogs(gatewayId) {
|
|
2396
|
+
try {
|
|
2397
|
+
await this.attemptAutoRelogin();
|
|
2398
|
+
const configManager = new config_1.ConfigManager();
|
|
2399
|
+
const gatewayInfo = configManager.getGateway(gatewayId);
|
|
2400
|
+
if (!gatewayInfo) {
|
|
2401
|
+
throw new Error('Gateway not found in local configuration');
|
|
2402
|
+
}
|
|
2403
|
+
// Load SSH key
|
|
2404
|
+
const awsService = new aws_1.AWSService(configManager.getAWSProfile(), gatewayInfo.region);
|
|
2405
|
+
const privateKey = awsService.loadSSHKey(gatewayInfo.keyPath);
|
|
2406
|
+
// Wait for instance to be ready for SSH
|
|
2407
|
+
const isReady = await awsService.waitForSSHReady(gatewayInfo.ec2InstanceId);
|
|
2408
|
+
if (!isReady) {
|
|
2409
|
+
throw new Error('Instance is not ready for SSH connections');
|
|
2410
|
+
}
|
|
2411
|
+
// Create SSH connection
|
|
2412
|
+
const connection = {
|
|
2413
|
+
host: gatewayInfo.publicIp,
|
|
2414
|
+
port: 22,
|
|
2415
|
+
username: 'ec2-user',
|
|
2416
|
+
privateKey: privateKey
|
|
2417
|
+
};
|
|
2418
|
+
// Test SSH connection
|
|
2419
|
+
await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
|
|
2420
|
+
// Log file locations (try both locations)
|
|
2421
|
+
const logPaths = [
|
|
2422
|
+
'/opt/edgible-agent/.edgible/agent/logs/agent.log',
|
|
2423
|
+
'~/.edgible/agent/logs/agent.log'
|
|
2424
|
+
];
|
|
2425
|
+
let wipedFiles = [];
|
|
2426
|
+
let errors = [];
|
|
2427
|
+
for (const logPath of logPaths) {
|
|
2428
|
+
try {
|
|
2429
|
+
// Check if file exists and wipe it
|
|
2430
|
+
const checkResult = await awsService.executeSSHCommand(connection, `test -f ${logPath} && echo "EXISTS" || echo "NOT_FOUND"`);
|
|
2431
|
+
if (checkResult.stdout.trim() === 'EXISTS') {
|
|
2432
|
+
// Wipe the log file (truncate to 0 bytes)
|
|
2433
|
+
await awsService.executeSSHCommand(connection, `sudo truncate -s 0 ${logPath} || sudo > ${logPath}`);
|
|
2434
|
+
wipedFiles.push(logPath);
|
|
2435
|
+
console.log(chalk_1.default.green(` ✓ Cleared ${logPath}`));
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
catch (error) {
|
|
2439
|
+
errors.push(`Failed to clear ${logPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
// Also clear systemd journal logs if requested (optional)
|
|
2443
|
+
try {
|
|
2444
|
+
await awsService.executeSSHCommand(connection, 'sudo journalctl --vacuum-time=1s -u edgible-agent.service || true');
|
|
2445
|
+
console.log(chalk_1.default.gray(' ✓ Cleared systemd journal logs'));
|
|
2446
|
+
}
|
|
2447
|
+
catch (error) {
|
|
2448
|
+
// Ignore journalctl errors
|
|
2449
|
+
}
|
|
2450
|
+
if (wipedFiles.length === 0 && errors.length > 0) {
|
|
2451
|
+
return {
|
|
2452
|
+
success: false,
|
|
2453
|
+
message: `No log files found or cleared. Errors: ${errors.join('; ')}`
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
return {
|
|
2457
|
+
success: true,
|
|
2458
|
+
logFile: wipedFiles.length > 0 ? wipedFiles[0] : undefined,
|
|
2459
|
+
message: wipedFiles.length > 0 ? `Cleared ${wipedFiles.length} log file(s)` : 'No log files found to clear'
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
catch (error) {
|
|
2463
|
+
console.error(chalk_1.default.red('Error wiping gateway logs:'), error);
|
|
2464
|
+
throw error;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Wipe agent log file on a managed gateway
|
|
2469
|
+
*/
|
|
2470
|
+
async wipeManagedGatewayLogs(gatewayId) {
|
|
2471
|
+
try {
|
|
2472
|
+
await this.attemptAutoRelogin();
|
|
2473
|
+
// Get managed gateway details
|
|
2474
|
+
const gatewayDetails = await this.getManagedGateway(gatewayId);
|
|
2475
|
+
if (!gatewayDetails.success || !gatewayDetails.gateway) {
|
|
2476
|
+
throw new Error('Managed gateway not found');
|
|
2477
|
+
}
|
|
2478
|
+
const gateway = gatewayDetails.gateway;
|
|
2479
|
+
const ipAddress = gateway.ipAddress;
|
|
2480
|
+
if (!ipAddress) {
|
|
2481
|
+
throw new Error('Gateway IP address not available');
|
|
2482
|
+
}
|
|
2483
|
+
// Get SSH key from backend
|
|
2484
|
+
const sshKeyResponse = await this.apiClient.getManagedGatewaySSHKey({ gatewayId });
|
|
2485
|
+
if (!sshKeyResponse.success || !sshKeyResponse.sshPrivateKey) {
|
|
2486
|
+
throw new Error('Failed to retrieve SSH key for managed gateway');
|
|
2487
|
+
}
|
|
2488
|
+
const sshPrivateKey = sshKeyResponse.sshPrivateKey;
|
|
2489
|
+
// Create SSH connection
|
|
2490
|
+
const connection = {
|
|
2491
|
+
host: ipAddress,
|
|
2492
|
+
port: 22,
|
|
2493
|
+
username: 'ec2-user',
|
|
2494
|
+
privateKey: sshPrivateKey
|
|
2495
|
+
};
|
|
2496
|
+
// Use AWSService to handle SSH operations
|
|
2497
|
+
const { AWSService } = await Promise.resolve().then(() => __importStar(require('./aws')));
|
|
2498
|
+
const awsService = new AWSService();
|
|
2499
|
+
// Test SSH connection
|
|
2500
|
+
await awsService.executeSSHCommand(connection, 'echo "SSH connection successful"');
|
|
2501
|
+
// Log file locations (try both locations)
|
|
2502
|
+
const logPaths = [
|
|
2503
|
+
'/opt/edgible-agent/.edgible/agent/logs/agent.log',
|
|
2504
|
+
'~/.edgible/agent/logs/agent.log'
|
|
2505
|
+
];
|
|
2506
|
+
let wipedFiles = [];
|
|
2507
|
+
let errors = [];
|
|
2508
|
+
for (const logPath of logPaths) {
|
|
2509
|
+
try {
|
|
2510
|
+
// Check if file exists and wipe it
|
|
2511
|
+
const checkResult = await awsService.executeSSHCommand(connection, `test -f ${logPath} && echo "EXISTS" || echo "NOT_FOUND"`);
|
|
2512
|
+
if (checkResult.stdout.trim() === 'EXISTS') {
|
|
2513
|
+
// Wipe the log file (truncate to 0 bytes)
|
|
2514
|
+
await awsService.executeSSHCommand(connection, `sudo truncate -s 0 ${logPath} || sudo > ${logPath}`);
|
|
2515
|
+
wipedFiles.push(logPath);
|
|
2516
|
+
console.log(chalk_1.default.green(` ✓ Cleared ${logPath}`));
|
|
2517
|
+
}
|
|
2518
|
+
}
|
|
2519
|
+
catch (error) {
|
|
2520
|
+
errors.push(`Failed to clear ${logPath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
// Also clear systemd journal logs if requested (optional)
|
|
2524
|
+
try {
|
|
2525
|
+
await awsService.executeSSHCommand(connection, 'sudo journalctl --vacuum-time=1s -u edgible-agent.service || true');
|
|
2526
|
+
console.log(chalk_1.default.gray(' ✓ Cleared systemd journal logs'));
|
|
2527
|
+
}
|
|
2528
|
+
catch (error) {
|
|
2529
|
+
// Ignore journalctl errors
|
|
2530
|
+
}
|
|
2531
|
+
if (wipedFiles.length === 0 && errors.length > 0) {
|
|
2532
|
+
return {
|
|
2533
|
+
success: false,
|
|
2534
|
+
message: `No log files found or cleared. Errors: ${errors.join('; ')}`
|
|
2535
|
+
};
|
|
2536
|
+
}
|
|
2537
|
+
return {
|
|
2538
|
+
success: true,
|
|
2539
|
+
logFile: wipedFiles.length > 0 ? wipedFiles[0] : undefined,
|
|
2540
|
+
message: wipedFiles.length > 0 ? `Cleared ${wipedFiles.length} log file(s)` : 'No log files found to clear'
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
catch (error) {
|
|
2544
|
+
console.error(chalk_1.default.red('Error wiping managed gateway logs:'), error);
|
|
2545
|
+
throw error;
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
/**
|
|
2549
|
+
* Get applications for a specific gateway
|
|
2550
|
+
*/
|
|
2551
|
+
async getGatewayApplications(gatewayId) {
|
|
2552
|
+
try {
|
|
2553
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
2554
|
+
await this.attemptAutoRelogin();
|
|
2555
|
+
const configManager = new config_1.ConfigManager();
|
|
2556
|
+
const userConfig = configManager.getConfig();
|
|
2557
|
+
if (!userConfig.organizationId) {
|
|
2558
|
+
throw new Error('No organization ID found. Please login first.');
|
|
2559
|
+
}
|
|
2560
|
+
// Get all applications from the organization
|
|
2561
|
+
const applicationsResponse = await this.apiClient.getOrganizationApplications(userConfig.organizationId);
|
|
2562
|
+
// Filter for applications that use this gateway
|
|
2563
|
+
const gatewayApplications = applicationsResponse.applications.filter((app) => app.deviceIds && app.deviceIds.includes(gatewayId));
|
|
2564
|
+
// Map API response to CLI Application format
|
|
2565
|
+
const applications = gatewayApplications.map((app) => ({
|
|
2566
|
+
id: app.id,
|
|
2567
|
+
name: app.name,
|
|
2568
|
+
workloadId: app.deviceIds[0] || 'unknown',
|
|
2569
|
+
servingIp: app.url ? new URL(app.url).hostname : 'unknown',
|
|
2570
|
+
port: app.url ? parseInt(new URL(app.url).port) || 80 : 80,
|
|
2571
|
+
protocol: app.url && app.url.startsWith('https') ? 'https' : 'http',
|
|
2572
|
+
status: app.status === 'running' ? 'active' : 'inactive',
|
|
2573
|
+
createdAt: app.createdAt,
|
|
2574
|
+
description: app.description
|
|
2575
|
+
}));
|
|
2576
|
+
return applications;
|
|
2577
|
+
}
|
|
2578
|
+
catch (error) {
|
|
2579
|
+
console.error(chalk_1.default.red('Error getting gateway applications:'), error);
|
|
2580
|
+
throw error;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
/**
|
|
2584
|
+
* Generate user data script for agent
|
|
2585
|
+
*/
|
|
2586
|
+
generateAgentUserData(deviceId) {
|
|
2587
|
+
const systemAgentPath = PathResolver_1.PathResolver.getSystemDataPath() + '/agent';
|
|
2588
|
+
return `#!/bin/bash
|
|
2589
|
+
# Update system
|
|
2590
|
+
yum update -y
|
|
2591
|
+
|
|
2592
|
+
# Install required system dependencies
|
|
2593
|
+
# Install iptables (usually pre-installed, but ensure it's there)
|
|
2594
|
+
yum install -y iptables
|
|
2595
|
+
|
|
2596
|
+
# Install WireGuard
|
|
2597
|
+
amazon-linux-extras install epel -y || yum install -y epel-release
|
|
2598
|
+
yum install -y wireguard-tools
|
|
2599
|
+
modprobe wireguard || true
|
|
2600
|
+
|
|
2601
|
+
# Install Caddy
|
|
2602
|
+
yum install -y tar curl grep
|
|
2603
|
+
CADDY_VERSION=$(curl -s "https://api.github.com/repos/caddyserver/caddy/releases/latest" | grep -Po '"tag_name": "\\K[^"]*')
|
|
2604
|
+
curl -1sLf "https://github.com/caddyserver/caddy/releases/download/\${CADDY_VERSION}/caddy_\${CADDY_VERSION}_linux_amd64.tar.gz" | tar -C /usr/bin -xzf - caddy
|
|
2605
|
+
chmod +x /usr/bin/caddy
|
|
2606
|
+
setcap cap_net_bind_service=+ep /usr/bin/caddy
|
|
2607
|
+
# Ensure it's in PATH
|
|
2608
|
+
ln -sf /usr/bin/caddy /usr/local/bin/caddy || true
|
|
2609
|
+
|
|
2610
|
+
# Install Node.js
|
|
2611
|
+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
|
2612
|
+
export NVM_DIR="$HOME/.nvm"
|
|
2613
|
+
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
|
2614
|
+
nvm install 18
|
|
2615
|
+
nvm use 18
|
|
2616
|
+
|
|
2617
|
+
# Create system-wide symlink for Node.js (so root can access it)
|
|
2618
|
+
NODE_VERSION=$(nvm list | grep -oE 'v18\.[0-9]+\.[0-9]+' | head -1)
|
|
2619
|
+
if [ -n "$NODE_VERSION" ]; then
|
|
2620
|
+
sudo ln -sf "/home/ec2-user/.nvm/versions/node/$NODE_VERSION/bin/node" /usr/local/bin/node
|
|
2621
|
+
sudo ln -sf "/home/ec2-user/.nvm/versions/node/$NODE_VERSION/bin/npm" /usr/local/bin/npm
|
|
2622
|
+
fi
|
|
2623
|
+
|
|
2624
|
+
# Install PM2 for process management (install globally for root as well)
|
|
2625
|
+
npm install -g pm2
|
|
2626
|
+
sudo npm install -g pm2 || true
|
|
2627
|
+
|
|
2628
|
+
# Create directory for agent (owned by root)
|
|
2629
|
+
sudo mkdir -p /opt/edgible-agent
|
|
2630
|
+
cd /opt/edgible-agent
|
|
2631
|
+
sudo chown root:root /opt/edgible-agent
|
|
2632
|
+
|
|
2633
|
+
# Set up systemd service for agent (running as root)
|
|
2634
|
+
cat > /tmp/edgible-agent.service << 'EOF'
|
|
2635
|
+
[Unit]
|
|
2636
|
+
Description=Edgible Agent
|
|
2637
|
+
After=network.target
|
|
2638
|
+
|
|
2639
|
+
[Service]
|
|
2640
|
+
Type=simple
|
|
2641
|
+
User=root
|
|
2642
|
+
WorkingDirectory=/opt/edgible-agent
|
|
2643
|
+
ExecStart=/usr/local/bin/node index.js start -c /opt/edgible-agent/agent.config.json
|
|
2644
|
+
Restart=always
|
|
2645
|
+
RestartSec=10
|
|
2646
|
+
Environment=NODE_ENV=production
|
|
2647
|
+
Environment=EDGIBLE_CONFIG_PATH=${systemAgentPath}
|
|
2648
|
+
Environment=DEVICE_ID=${deviceId}
|
|
2649
|
+
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
|
2650
|
+
|
|
2651
|
+
[Install]
|
|
2652
|
+
WantedBy=multi-user.target
|
|
2653
|
+
EOF
|
|
2654
|
+
sudo mv /tmp/edgible-agent.service /etc/systemd/system/edgible-agent.service
|
|
2655
|
+
|
|
2656
|
+
# Enable service
|
|
2657
|
+
systemctl enable edgible-agent.service
|
|
2658
|
+
|
|
2659
|
+
echo "Edgible Agent setup complete for device: ${deviceId}"
|
|
2660
|
+
`;
|
|
2661
|
+
}
|
|
2662
|
+
/**
|
|
2663
|
+
* Generate placeholder agent code
|
|
2664
|
+
*/
|
|
2665
|
+
generateAgentCode(deviceId) {
|
|
2666
|
+
return `#!/usr/bin/env node
|
|
2667
|
+
|
|
2668
|
+
console.log('Edgible Agent starting...');
|
|
2669
|
+
console.log('Device ID:', process.env.DEVICE_ID || '${deviceId}');
|
|
2670
|
+
|
|
2671
|
+
// Placeholder agent code
|
|
2672
|
+
// In a real implementation, this would be the actual agent-v2 code
|
|
2673
|
+
|
|
2674
|
+
setInterval(() => {
|
|
2675
|
+
console.log('Agent heartbeat:', new Date().toISOString());
|
|
2676
|
+
}, 30000);
|
|
2677
|
+
|
|
2678
|
+
console.log('Edgible Agent started successfully');
|
|
2679
|
+
`;
|
|
2680
|
+
}
|
|
2681
|
+
// Enhanced Application Setup with Validation
|
|
2682
|
+
/**
|
|
2683
|
+
* Enhanced setup with full validation
|
|
2684
|
+
*/
|
|
2685
|
+
async setupApplicationWithValidation(workload, port, protocol = 'http', description, gatewayId) {
|
|
2686
|
+
const startTime = Date.now();
|
|
2687
|
+
const warnings = [];
|
|
2688
|
+
const errors = [];
|
|
2689
|
+
try {
|
|
2690
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
2691
|
+
await this.attemptAutoRelogin();
|
|
2692
|
+
console.log(chalk_1.default.blue('Setting up application with validation...'));
|
|
2693
|
+
// Step 1: Check local agent status
|
|
2694
|
+
console.log(chalk_1.default.gray('Checking local agent status...'));
|
|
2695
|
+
const localAgentManager = new LocalAgentManager_1.LocalAgentManager();
|
|
2696
|
+
let localAgentStatus = await localAgentManager.checkLocalAgentStatus();
|
|
2697
|
+
if (!localAgentStatus.installed) {
|
|
2698
|
+
warnings.push('Local serving agent is not installed');
|
|
2699
|
+
console.log(chalk_1.default.yellow('⚠ Local serving agent not found. Installing...'));
|
|
2700
|
+
const installResult = await localAgentManager.installLocalAgent({
|
|
2701
|
+
autoStart: true
|
|
2702
|
+
});
|
|
2703
|
+
if (!installResult.success) {
|
|
2704
|
+
errors.push(`Failed to install local agent: ${installResult.error}`);
|
|
2705
|
+
}
|
|
2706
|
+
else {
|
|
2707
|
+
console.log(chalk_1.default.green('✓ Local agent installed successfully'));
|
|
2708
|
+
// Wait and verify the agent is running
|
|
2709
|
+
console.log(chalk_1.default.gray('Verifying agent is running...'));
|
|
2710
|
+
let retries = 0;
|
|
2711
|
+
const maxRetries = 5;
|
|
2712
|
+
let isRunning = false;
|
|
2713
|
+
while (retries < maxRetries && !isRunning) {
|
|
2714
|
+
await new Promise(resolve => setTimeout(resolve, 1000)); // Wait 1 second
|
|
2715
|
+
localAgentStatus = await localAgentManager.checkLocalAgentStatus();
|
|
2716
|
+
isRunning = localAgentStatus.running;
|
|
2717
|
+
retries++;
|
|
2718
|
+
if (!isRunning) {
|
|
2719
|
+
console.log(chalk_1.default.gray(`Waiting for agent to start... (${retries}/${maxRetries})`));
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2722
|
+
if (!isRunning) {
|
|
2723
|
+
warnings.push('Local serving agent installation completed but may not be running');
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
else if (!localAgentStatus.running) {
|
|
2728
|
+
warnings.push('Local serving agent is not running');
|
|
2729
|
+
console.log(chalk_1.default.yellow('⚠ Starting local serving agent...'));
|
|
2730
|
+
const started = await localAgentManager.startLocalAgent();
|
|
2731
|
+
if (!started) {
|
|
2732
|
+
errors.push('Failed to start local serving agent');
|
|
2733
|
+
}
|
|
2734
|
+
else {
|
|
2735
|
+
console.log(chalk_1.default.green('✓ Local serving agent started'));
|
|
2736
|
+
// Verify the agent is actually running
|
|
2737
|
+
localAgentStatus = await localAgentManager.checkLocalAgentStatus();
|
|
2738
|
+
if (!localAgentStatus.running) {
|
|
2739
|
+
warnings.push('Agent start command succeeded but agent may not be running');
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
else {
|
|
2744
|
+
console.log(chalk_1.default.green('✓ Local serving agent is running'));
|
|
2745
|
+
}
|
|
2746
|
+
// Final status check
|
|
2747
|
+
if (errors.length === 0 && !localAgentStatus.running) {
|
|
2748
|
+
warnings.push('Local serving agent may not be running properly');
|
|
2749
|
+
}
|
|
2750
|
+
// Step 2: Create application
|
|
2751
|
+
console.log(chalk_1.default.gray('Creating application...'));
|
|
2752
|
+
const application = await this.setupApplication(workload, port, protocol, description, gatewayId);
|
|
2753
|
+
// Step 3: Configure local agent for application
|
|
2754
|
+
console.log(chalk_1.default.gray('Configuring local agent for application...'));
|
|
2755
|
+
const configResult = await localAgentManager.configureAgentForApplication(application);
|
|
2756
|
+
if (!configResult.success) {
|
|
2757
|
+
warnings.push(`Failed to configure agent: ${configResult.error}`);
|
|
2758
|
+
}
|
|
2759
|
+
// Step 4: Test connectivity
|
|
2760
|
+
console.log(chalk_1.default.gray('Testing connectivity...'));
|
|
2761
|
+
const connectivityTester = new ConnectivityTester_1.ConnectivityTester();
|
|
2762
|
+
let connectivityTest;
|
|
2763
|
+
if (gatewayId) {
|
|
2764
|
+
// Get gateway information
|
|
2765
|
+
const gatewaysResponse = await this.listGateways();
|
|
2766
|
+
const gateway = gatewaysResponse.gateways.find((g) => g.device.id === gatewayId);
|
|
2767
|
+
if (gateway) {
|
|
2768
|
+
const gatewayInfo = {
|
|
2769
|
+
id: gateway.device.id,
|
|
2770
|
+
name: gateway.device.name,
|
|
2771
|
+
publicIp: gateway.ec2Instance?.publicIp || '',
|
|
2772
|
+
privateIp: gateway.ec2Instance?.privateIp,
|
|
2773
|
+
region: gateway.ec2Instance?.region || 'us-east-1',
|
|
2774
|
+
status: gateway.device.status,
|
|
2775
|
+
deviceId: gateway.device.id,
|
|
2776
|
+
ec2InstanceId: gateway.ec2Instance?.instanceId
|
|
2777
|
+
};
|
|
2778
|
+
connectivityTest = await connectivityTester.testGatewayToWorkloadConnectivity(gatewayInfo, workload, port);
|
|
2779
|
+
}
|
|
2780
|
+
else {
|
|
2781
|
+
// Fallback connectivity test without gateway
|
|
2782
|
+
const portTest = await connectivityTester.testPortConnectivity(workload.ipAddress || 'localhost', port, 'tcp', 5000);
|
|
2783
|
+
connectivityTest = {
|
|
2784
|
+
overall: portTest.success,
|
|
2785
|
+
tests: [{
|
|
2786
|
+
success: portTest.success,
|
|
2787
|
+
applicationId: application.id,
|
|
2788
|
+
gatewayIp: 'localhost',
|
|
2789
|
+
workloadIp: workload.ipAddress || 'localhost',
|
|
2790
|
+
port,
|
|
2791
|
+
protocol: 'tcp',
|
|
2792
|
+
testType: 'connectivity',
|
|
2793
|
+
host: workload.ipAddress || 'localhost',
|
|
2794
|
+
latency: portTest.latency,
|
|
2795
|
+
error: portTest.error,
|
|
2796
|
+
timestamp: portTest.timestamp,
|
|
2797
|
+
url: `tcp://${workload.ipAddress || 'localhost'}:${port}`,
|
|
2798
|
+
method: 'CONNECT'
|
|
2799
|
+
}],
|
|
2800
|
+
diagnostics: {
|
|
2801
|
+
networkIssues: [],
|
|
2802
|
+
configurationIssues: [],
|
|
2803
|
+
serviceIssues: [],
|
|
2804
|
+
suggestions: []
|
|
2805
|
+
},
|
|
2806
|
+
recommendations: [],
|
|
2807
|
+
timestamp: new Date(),
|
|
2808
|
+
duration: 0
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
else {
|
|
2813
|
+
// Test local connectivity only
|
|
2814
|
+
const portTest = await connectivityTester.testPortConnectivity(workload.ipAddress || 'localhost', port, 'tcp', 5000);
|
|
2815
|
+
connectivityTest = {
|
|
2816
|
+
overall: portTest.success,
|
|
2817
|
+
tests: [{
|
|
2818
|
+
success: portTest.success,
|
|
2819
|
+
applicationId: application.id,
|
|
2820
|
+
gatewayIp: 'localhost',
|
|
2821
|
+
workloadIp: workload.ipAddress || 'localhost',
|
|
2822
|
+
port,
|
|
2823
|
+
protocol: 'tcp',
|
|
2824
|
+
testType: 'connectivity',
|
|
2825
|
+
host: workload.ipAddress || 'localhost',
|
|
2826
|
+
latency: portTest.latency,
|
|
2827
|
+
error: portTest.error,
|
|
2828
|
+
timestamp: portTest.timestamp,
|
|
2829
|
+
url: `tcp://${workload.ipAddress || 'localhost'}:${port}`,
|
|
2830
|
+
method: 'CONNECT'
|
|
2831
|
+
}],
|
|
2832
|
+
diagnostics: {
|
|
2833
|
+
networkIssues: [],
|
|
2834
|
+
configurationIssues: [],
|
|
2835
|
+
serviceIssues: [],
|
|
2836
|
+
suggestions: []
|
|
2837
|
+
},
|
|
2838
|
+
recommendations: [],
|
|
2839
|
+
timestamp: new Date(),
|
|
2840
|
+
duration: 0
|
|
2841
|
+
};
|
|
2842
|
+
}
|
|
2843
|
+
if (!connectivityTest.overall) {
|
|
2844
|
+
errors.push('Connectivity test failed');
|
|
2845
|
+
console.log(chalk_1.default.red('✗ Connectivity test failed'));
|
|
2846
|
+
connectivityTest.recommendations.forEach(rec => {
|
|
2847
|
+
console.log(chalk_1.default.yellow(` • ${rec}`));
|
|
2848
|
+
});
|
|
2849
|
+
}
|
|
2850
|
+
else {
|
|
2851
|
+
console.log(chalk_1.default.green('✓ Connectivity test passed'));
|
|
2852
|
+
}
|
|
2853
|
+
// Step 5: Health validation
|
|
2854
|
+
console.log(chalk_1.default.gray('Performing health validation...'));
|
|
2855
|
+
const healthValidation = await this.getApplicationHealth(application.id);
|
|
2856
|
+
if (healthValidation.overall !== 'healthy') {
|
|
2857
|
+
warnings.push('Application health check failed');
|
|
2858
|
+
console.log(chalk_1.default.yellow('⚠ Application health check failed'));
|
|
2859
|
+
}
|
|
2860
|
+
else {
|
|
2861
|
+
console.log(chalk_1.default.green('✓ Application health check passed'));
|
|
2862
|
+
}
|
|
2863
|
+
// Step 6: Final status
|
|
2864
|
+
const setupComplete = errors.length === 0;
|
|
2865
|
+
const duration = Date.now() - startTime;
|
|
2866
|
+
if (setupComplete) {
|
|
2867
|
+
console.log(chalk_1.default.green('\n✓ Application setup completed successfully!'));
|
|
2868
|
+
}
|
|
2869
|
+
else {
|
|
2870
|
+
console.log(chalk_1.default.red('\n✗ Application setup completed with errors'));
|
|
2871
|
+
}
|
|
2872
|
+
return {
|
|
2873
|
+
application: application,
|
|
2874
|
+
localAgentStatus: localAgentStatus,
|
|
2875
|
+
connectivityTest,
|
|
2876
|
+
healthValidation,
|
|
2877
|
+
setupComplete,
|
|
2878
|
+
warnings,
|
|
2879
|
+
errors,
|
|
2880
|
+
duration,
|
|
2881
|
+
timestamp: new Date()
|
|
2882
|
+
};
|
|
2883
|
+
}
|
|
2884
|
+
catch (error) {
|
|
2885
|
+
const duration = Date.now() - startTime;
|
|
2886
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
2887
|
+
return {
|
|
2888
|
+
application: {},
|
|
2889
|
+
localAgentStatus: {
|
|
2890
|
+
running: false,
|
|
2891
|
+
health: 'unknown',
|
|
2892
|
+
uptime: 0,
|
|
2893
|
+
version: 'unknown',
|
|
2894
|
+
timestamp: Date.now(),
|
|
2895
|
+
deviceId: '',
|
|
2896
|
+
deviceType: 'serving',
|
|
2897
|
+
apiConnected: false,
|
|
2898
|
+
apiAuthenticated: false,
|
|
2899
|
+
applications: [],
|
|
2900
|
+
configurationValid: false,
|
|
2901
|
+
installed: false,
|
|
2902
|
+
lastError: errorMessage,
|
|
2903
|
+
pid: 0
|
|
2904
|
+
},
|
|
2905
|
+
connectivityTest: {
|
|
2906
|
+
overall: false,
|
|
2907
|
+
tests: [],
|
|
2908
|
+
diagnostics: {
|
|
2909
|
+
networkIssues: [],
|
|
2910
|
+
configurationIssues: [],
|
|
2911
|
+
serviceIssues: [],
|
|
2912
|
+
suggestions: []
|
|
2913
|
+
},
|
|
2914
|
+
recommendations: [],
|
|
2915
|
+
timestamp: new Date(),
|
|
2916
|
+
duration: 0
|
|
2917
|
+
},
|
|
2918
|
+
healthValidation: {
|
|
2919
|
+
overall: 'unknown',
|
|
2920
|
+
checks: [],
|
|
2921
|
+
lastChecked: new Date(),
|
|
2922
|
+
uptime: 0,
|
|
2923
|
+
version: 'unknown'
|
|
2924
|
+
},
|
|
2925
|
+
setupComplete: false,
|
|
2926
|
+
warnings,
|
|
2927
|
+
errors: [errorMessage],
|
|
2928
|
+
duration,
|
|
2929
|
+
timestamp: new Date()
|
|
2930
|
+
};
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
/**
|
|
2934
|
+
* Test application after setup
|
|
2935
|
+
*/
|
|
2936
|
+
async validateApplicationSetup(application) {
|
|
2937
|
+
const checks = [];
|
|
2938
|
+
const recommendations = [];
|
|
2939
|
+
try {
|
|
2940
|
+
// Check 1: Application exists
|
|
2941
|
+
checks.push({
|
|
2942
|
+
name: 'application_exists',
|
|
2943
|
+
status: application.id ? 'pass' : 'fail',
|
|
2944
|
+
message: application.id ? 'Application exists' : 'Application not found',
|
|
2945
|
+
duration: 0
|
|
2946
|
+
});
|
|
2947
|
+
// Check 2: Port connectivity
|
|
2948
|
+
const connectivityTester = new ConnectivityTester_1.ConnectivityTester();
|
|
2949
|
+
const portTest = await connectivityTester.testPortConnectivity(application.servingIp, application.port, 'tcp', 5000);
|
|
2950
|
+
checks.push({
|
|
2951
|
+
name: 'port_connectivity',
|
|
2952
|
+
status: portTest.success ? 'pass' : 'fail',
|
|
2953
|
+
message: portTest.success ? 'Port is accessible' : `Port connectivity failed: ${portTest.error}`,
|
|
2954
|
+
duration: portTest.latency || 0
|
|
2955
|
+
});
|
|
2956
|
+
// Check 3: HTTP endpoint (if applicable)
|
|
2957
|
+
if (application.protocol === 'http' || application.protocol === 'https') {
|
|
2958
|
+
const url = `${application.protocol}://${application.servingIp}:${application.port}`;
|
|
2959
|
+
const httpTest = await connectivityTester.testHttpEndpoint(url, 200, 5000);
|
|
2960
|
+
checks.push({
|
|
2961
|
+
name: 'http_endpoint',
|
|
2962
|
+
status: httpTest.success ? 'pass' : 'fail',
|
|
2963
|
+
message: httpTest.success ? 'HTTP endpoint is responding' : `HTTP endpoint failed: ${httpTest.error}`,
|
|
2964
|
+
duration: httpTest.responseTime || 0
|
|
2965
|
+
});
|
|
2966
|
+
}
|
|
2967
|
+
// Check 4: Local agent status
|
|
2968
|
+
const localAgentManager = new LocalAgentManager_1.LocalAgentManager();
|
|
2969
|
+
const agentStatus = await localAgentManager.checkLocalAgentStatus();
|
|
2970
|
+
checks.push({
|
|
2971
|
+
name: 'local_agent',
|
|
2972
|
+
status: agentStatus.running ? 'pass' : 'warn',
|
|
2973
|
+
message: agentStatus.running ? 'Local agent is running' : 'Local agent is not running',
|
|
2974
|
+
duration: 0
|
|
2975
|
+
});
|
|
2976
|
+
// Generate recommendations
|
|
2977
|
+
if (!portTest.success) {
|
|
2978
|
+
recommendations.push('Check if the workload service is running and listening on the correct port');
|
|
2979
|
+
recommendations.push('Verify firewall settings allow connections to the port');
|
|
2980
|
+
}
|
|
2981
|
+
if (agentStatus.installed && !agentStatus.running) {
|
|
2982
|
+
recommendations.push('Start the local serving agent');
|
|
2983
|
+
}
|
|
2984
|
+
const overall = checks.every(c => c.status === 'pass') ? 'pass' :
|
|
2985
|
+
checks.some(c => c.status === 'fail') ? 'fail' : 'warn';
|
|
2986
|
+
return {
|
|
2987
|
+
success: overall === 'pass',
|
|
2988
|
+
checks,
|
|
2989
|
+
overall,
|
|
2990
|
+
recommendations
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2993
|
+
catch (error) {
|
|
2994
|
+
return {
|
|
2995
|
+
success: false,
|
|
2996
|
+
checks: [{
|
|
2997
|
+
name: 'validation_error',
|
|
2998
|
+
status: 'fail',
|
|
2999
|
+
message: error instanceof Error ? error.message : 'Validation failed',
|
|
3000
|
+
duration: 0
|
|
3001
|
+
}],
|
|
3002
|
+
overall: 'fail',
|
|
3003
|
+
recommendations: ['Check application configuration and try again']
|
|
3004
|
+
};
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
/**
|
|
3008
|
+
* Get application health status
|
|
3009
|
+
*/
|
|
3010
|
+
async getApplicationHealth(applicationId) {
|
|
3011
|
+
try {
|
|
3012
|
+
const localAgentManager = new LocalAgentManager_1.LocalAgentManager();
|
|
3013
|
+
return await localAgentManager.monitorAgentHealth();
|
|
3014
|
+
}
|
|
3015
|
+
catch (error) {
|
|
3016
|
+
return {
|
|
3017
|
+
overall: 'unknown',
|
|
3018
|
+
checks: [{
|
|
3019
|
+
name: 'health_check_error',
|
|
3020
|
+
status: 'fail',
|
|
3021
|
+
message: error instanceof Error ? error.message : 'Health check failed',
|
|
3022
|
+
timestamp: new Date()
|
|
3023
|
+
}],
|
|
3024
|
+
lastChecked: new Date(),
|
|
3025
|
+
uptime: 0,
|
|
3026
|
+
version: 'unknown'
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
/**
|
|
3031
|
+
* Create application programmatically without interactive prompts
|
|
3032
|
+
*/
|
|
3033
|
+
async createApplicationProgrammatically(params) {
|
|
3034
|
+
try {
|
|
3035
|
+
// Attempt auto re-login if tokens are missing but credentials are available
|
|
3036
|
+
await this.attemptAutoRelogin();
|
|
3037
|
+
const config = this.configManager.getConfig();
|
|
3038
|
+
if (!config.organizationId) {
|
|
3039
|
+
throw new Error('Not logged in. Please run "edgible login" first.');
|
|
3040
|
+
}
|
|
3041
|
+
const createRequest = {
|
|
3042
|
+
name: params.name,
|
|
3043
|
+
description: params.description,
|
|
3044
|
+
organizationId: config.organizationId,
|
|
3045
|
+
configuration: { port: params.port, protocol: params.protocol },
|
|
3046
|
+
hostnames: params.hostnames,
|
|
3047
|
+
deviceIds: params.deviceIds,
|
|
3048
|
+
gatewayIds: params.gatewayIds,
|
|
3049
|
+
subtype: params.subtype
|
|
3050
|
+
};
|
|
3051
|
+
const response = await this.apiClient.createApplication(createRequest);
|
|
3052
|
+
return response.application;
|
|
3053
|
+
}
|
|
3054
|
+
catch (error) {
|
|
3055
|
+
throw new Error(`Failed to create application: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
exports.EdgibleService = EdgibleService;
|
|
3060
|
+
//# sourceMappingURL=edgible.js.map
|