Skip to content

Commit 599038d

Browse files
authored
feat: register RHEL VM at startup (#188)
1 parent c744d66 commit 599038d

File tree

5 files changed

+256
-11
lines changed

5 files changed

+256
-11
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@
4545
"description": "Image Path",
4646
"when": "rhel-vms.localImage"
4747
},
48+
"rhel-vms.factory.machine.register": {
49+
"type": "boolean",
50+
"scope": "VmProviderConnectionFactory",
51+
"default": true,
52+
"description": "Register machine via subscription-manager"
53+
},
4854
"rhel-vms.factory.machine.cpus": {
4955
"type": "number",
5056
"format": "cpu",

src/extension.spec.ts

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import { resolve } from 'node:path';
2020
import * as macadamJSPackage from '@crc-org/macadam.js';
2121
import * as extensionApi from '@podman-desktop/api';
2222
import { vol } from 'memfs';
23-
import { assert, beforeEach, describe, expect, test, vi } from 'vitest';
23+
import { afterEach, assert, beforeEach, describe, expect, test, vi } from 'vitest';
2424

2525
import * as authentication from './authentication';
2626
import { ImageCache } from './cache';
@@ -48,6 +48,7 @@ vi.mock('@crc-org/macadam.js', async () => {
4848
Macadam.prototype.startVm = vi.fn();
4949
Macadam.prototype.stopVm = vi.fn();
5050
Macadam.prototype.removeVm = vi.fn();
51+
Macadam.prototype.executeCommand = vi.fn();
5152
return { Macadam };
5253
});
5354
vi.mock('./macadam-machine-stream.js', async () => {
@@ -137,6 +138,7 @@ describe('activate', () => {
137138
);
138139
await create({
139140
'rhel-vms.factory.machine.image': 'RHEL 10',
141+
'rhel-vms.factory.machine.register': false,
140142
});
141143
expect(utils.pullImageFromRedHatRegistry).toHaveBeenCalled();
142144
});
@@ -148,6 +150,7 @@ describe('activate', () => {
148150
await create({
149151
'rhel-vms.factory.machine.image': 'RHEL 10',
150152
'rhel-vms.factory.machine.force-download': true,
153+
'rhel-vms.factory.machine.register': false,
151154
});
152155
vol.fromJSON({
153156
'/path/to/storage/images/rhel10': '',
@@ -165,6 +168,7 @@ describe('activate', () => {
165168
});
166169
await create({
167170
'rhel-vms.factory.machine.image': 'RHEL 10',
171+
'rhel-vms.factory.machine.register': false,
168172
});
169173
expect(utils.pullImageFromRedHatRegistry).not.toHaveBeenCalled();
170174
});
@@ -176,6 +180,7 @@ describe('activate', () => {
176180
await create({
177181
'rhel-vms.factory.machine.name': 'name1',
178182
'rhel-vms.factory.machine.image': 'RHEL 10',
183+
'rhel-vms.factory.machine.register': false,
179184
});
180185
expect(macadamJSPackage.Macadam.prototype.createVm).toHaveBeenCalledWith({
181186
containerProvider: 'applehv',
@@ -191,6 +196,7 @@ describe('activate', () => {
191196
'rhel-vms.factory.machine.image': 'local image on disk',
192197
'rhel-vms.factory.machine.name': 'name1',
193198
'rhel-vms.factory.machine.image-path': resolve('/', 'path', 'to', 'provided', 'image'),
199+
'rhel-vms.factory.machine.register': false,
194200
});
195201
expect(macadamJSPackage.Macadam.prototype.createVm).toHaveBeenCalledWith({
196202
containerProvider: 'applehv',
@@ -221,6 +227,7 @@ describe('activate', () => {
221227
);
222228
await create({
223229
'rhel-vms.factory.machine.image': 'RHEL 10',
230+
'rhel-vms.factory.machine.register': false,
224231
});
225232
expect(utils.pullImageFromRedHatRegistry).toHaveBeenCalled();
226233
});
@@ -234,6 +241,7 @@ describe('activate', () => {
234241
});
235242
await create({
236243
'rhel-vms.factory.machine.image': 'RHEL 10',
244+
'rhel-vms.factory.machine.register': false,
237245
});
238246
expect(utils.pullImageFromRedHatRegistry).not.toHaveBeenCalled();
239247
});
@@ -245,6 +253,7 @@ describe('activate', () => {
245253
await create({
246254
'rhel-vms.factory.machine.name': 'name1',
247255
'rhel-vms.factory.machine.image': 'RHEL 10',
256+
'rhel-vms.factory.machine.register': false,
248257
});
249258
expect(macadamJSPackage.Macadam.prototype.createVm).toHaveBeenCalledWith({
250259
containerProvider: 'wsl',
@@ -260,6 +269,7 @@ describe('activate', () => {
260269
'rhel-vms.factory.machine.image': 'local image on disk',
261270
'rhel-vms.factory.machine.name': 'name1',
262271
'rhel-vms.factory.machine.image-path': resolve('/', 'path', 'to', 'provided', 'image'),
272+
'rhel-vms.factory.machine.register': false,
263273
});
264274
expect(macadamJSPackage.Macadam.prototype.createVm).toHaveBeenCalledWith({
265275
containerProvider: 'wsl',
@@ -288,6 +298,7 @@ describe('activate', () => {
288298
await expect(
289299
create({
290300
'rhel-vms.factory.machine.image': 'RHEL 10',
301+
'rhel-vms.factory.machine.register': false,
291302
}),
292303
).rejects.toThrowError('provider hyperv is not supported');
293304
});
@@ -304,6 +315,7 @@ describe('activate', () => {
304315
await expect(
305316
create({
306317
'rhel-vms.factory.machine.image': 'RHEL 10',
318+
'rhel-vms.factory.machine.register': false,
307319
}),
308320
).rejects.toThrowError('an init error');
309321
});
@@ -517,3 +529,104 @@ bla bla
517529
});
518530
});
519531
});
532+
533+
describe('register', () => {
534+
let create: (
535+
params: {
536+
[key: string]: unknown;
537+
},
538+
logger?: extensionApi.Logger,
539+
token?: extensionApi.CancellationToken,
540+
) => Promise<void>;
541+
542+
const extensionContext: extensionApi.ExtensionContext = {
543+
subscriptions: {
544+
push: vi.fn(),
545+
},
546+
storagePath: resolve('/', 'path', 'to', 'storage'),
547+
} as unknown as extensionApi.ExtensionContext;
548+
549+
const provider: extensionApi.Provider = {
550+
setVmProviderConnectionFactory: vi.fn(),
551+
registerVmProviderConnection: vi.fn(),
552+
updateStatus: vi.fn(),
553+
} as unknown as extensionApi.Provider;
554+
555+
const authClient: SubscriptionManagerClientV1 = {
556+
images: {
557+
downloadImageUsingSha: vi.fn(),
558+
},
559+
} as unknown as SubscriptionManagerClientV1;
560+
561+
beforeEach(async () => {
562+
vi.useFakeTimers();
563+
vi.mocked(extensionApi.provider.createProvider).mockReturnValue(provider);
564+
565+
vi.mocked(extensionApi.env).isMac = true;
566+
vi.mocked(extensionApi.env).isWindows = false;
567+
vi.mocked(authentication.initAuthentication).mockResolvedValue(authClient);
568+
569+
await activate(extensionContext);
570+
expect(provider.setVmProviderConnectionFactory).toHaveBeenCalledOnce();
571+
const call = vi.mocked(provider.setVmProviderConnectionFactory).mock.calls[0];
572+
assert(!!call[0].create);
573+
create = call[0].create;
574+
});
575+
576+
afterEach(() => {
577+
vi.useRealTimers();
578+
});
579+
580+
test('register is true and machine is started and registered', async () => {
581+
const authClient: SubscriptionManagerClientV1 = {
582+
getOrganizationId: vi.fn().mockReturnValue('123456'),
583+
} as unknown as SubscriptionManagerClientV1;
584+
vi.mocked(authentication.initAuthentication).mockResolvedValue(authClient);
585+
vi.mocked(macadamJSPackage.Macadam.prototype.listVms).mockResolvedValue([
586+
{
587+
Name: 'name1',
588+
Image: '/path/to/image1',
589+
CPUs: 1,
590+
Memory: '1GB',
591+
DiskSize: '1GB',
592+
Running: true,
593+
Starting: true,
594+
Port: 80,
595+
RemoteUsername: 'user',
596+
IdentityPath: '/path/to/id1',
597+
VMType: 'applehv',
598+
},
599+
]);
600+
vi.mocked(macadamJSPackage.Macadam.prototype.executeCommand).mockResolvedValue({
601+
stdout: 'done',
602+
} as extensionApi.RunResult);
603+
const createPromise = create({
604+
'rhel-vms.factory.machine.name': 'name1',
605+
'rhel-vms.factory.machine.image': 'RHEL 10',
606+
'rhel-vms.factory.machine.register': true,
607+
});
608+
vi.mocked(macadamJSPackage.Macadam.prototype.listVms).mockResolvedValue([
609+
{
610+
Name: 'name1',
611+
Image: '/path/to/image1',
612+
CPUs: 1,
613+
Memory: '1GB',
614+
DiskSize: '1GB',
615+
Running: true,
616+
Starting: false,
617+
Port: 80,
618+
RemoteUsername: 'user',
619+
IdentityPath: '/path/to/id1',
620+
VMType: 'applehv',
621+
},
622+
]);
623+
vi.advanceTimersToNextTimer();
624+
await createPromise;
625+
expect(macadamJSPackage.Macadam.prototype.executeCommand).toHaveBeenCalledWith({
626+
name: 'name1',
627+
command: 'sudo',
628+
args: ['subscription-manager', 'register', '--force', '--activationkey', 'podman-desktop', '--org', '123456'],
629+
runOptions: {},
630+
});
631+
});
632+
});

src/extension.ts

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ async function createProvider(extensionContext: extensionApi.ExtensionContext):
444444
logger?: extensionApi.Logger,
445445
token?: extensionApi.CancellationToken,
446446
) => {
447-
return createVM(imageCache, params, logger, token);
447+
return createVM(provider, imageCache, params, logger, token);
448448
},
449449
creationDisplayName: 'Virtual machine',
450450
},
@@ -457,6 +457,7 @@ async function createProvider(extensionContext: extensionApi.ExtensionContext):
457457
}
458458

459459
async function createVM(
460+
provider: extensionApi.Provider,
460461
imageCache: ImageCache,
461462
// eslint-disable-next-line @typescript-eslint/no-explicit-any
462463
params: { [key: string]: any },
@@ -470,17 +471,17 @@ async function createVM(
470471
telemetryRecords.OS = 'win';
471472
}
472473

473-
let provider: 'wsl' | 'hyperv' | 'applehv' | undefined;
474+
let containerProvider: 'wsl' | 'hyperv' | 'applehv' | undefined;
474475
if (params['rhel-vms.factory.machine.win.provider']) {
475-
provider = params['rhel-vms.factory.machine.win.provider'];
476-
telemetryRecords.provider = provider;
476+
containerProvider = params['rhel-vms.factory.machine.win.provider'];
477+
telemetryRecords.provider = containerProvider;
477478
} else {
478479
if (extensionApi.env.isWindows) {
479-
provider = (await isWSLEnabled()) ? 'wsl' : 'hyperv';
480-
telemetryRecords.provider = provider;
480+
containerProvider = (await isWSLEnabled()) ? 'wsl' : 'hyperv';
481+
telemetryRecords.provider = containerProvider;
481482
} else if (extensionApi.env.isMac) {
482-
provider = 'applehv';
483-
telemetryRecords.provider = provider;
483+
containerProvider = 'applehv';
484+
telemetryRecords.provider = containerProvider;
484485
}
485486
}
486487

@@ -506,6 +507,12 @@ async function createVM(
506507
throw new Error('force-download must be a boolean');
507508
}
508509

510+
// register
511+
const register = params['rhel-vms.factory.machine.register'] ?? true;
512+
if (typeof register !== 'boolean') {
513+
throw new Error('register must be a boolean');
514+
}
515+
509516
if (image) {
510517
const cachedImagePath = imageCache.getPath(image);
511518
if (!forceDownload && existsSync(cachedImagePath)) {
@@ -514,7 +521,7 @@ async function createVM(
514521
logger?.log(`Using image cached in ${cachedImagePath}\n`);
515522
} else {
516523
const client = await initAuthentication();
517-
const imageSha = getImageSha(provider, image);
524+
const imageSha = getImageSha(containerProvider, image);
518525
logger?.log('Downloading image, please wait...\n');
519526
await pullImageFromRedHatRegistry(client, imageSha, cachedImagePath, logger, token);
520527
logger?.log(`Image downloaded\n`);
@@ -530,10 +537,12 @@ async function createVM(
530537
name: name,
531538
imagePath: imagePath,
532539
username: 'core',
533-
containerProvider: provider,
540+
containerProvider,
534541
runOptions: { logger, token },
535542
});
543+
logger?.log('VM created\n');
536544
} catch (error) {
545+
logger?.log('VM creation failed\n');
537546
telemetryRecords.error = error;
538547
const runError = error as extensionApi.RunError;
539548

@@ -547,6 +556,77 @@ async function createVM(
547556
//in the POC we do not send any telemetry
548557
//sendTelemetryRecords('macadam.machine.init', telemetryRecords, false);
549558
}
559+
560+
if (register) {
561+
await startAndRegisterVM(provider, containerProvider, name, logger);
562+
}
563+
}
564+
565+
/* startAndRegisterVM starts a VM and registers it via subscription-manager
566+
* Limitation: the machine must be stopped
567+
*/
568+
async function startAndRegisterVM(
569+
provider: extensionApi.Provider,
570+
containerProvider: 'wsl' | 'hyperv' | 'applehv' | undefined,
571+
name: string,
572+
logger?: extensionApi.Logger,
573+
): Promise<void> {
574+
const list = await macadam.listVms({ containerProvider: containerProvider });
575+
const machineInfo = list.find(info => info.Name === name);
576+
if (!machineInfo) {
577+
throw new Error(`Machine ${name} not found`);
578+
}
579+
580+
const startedPromise = new Promise<void>((resolve, reject) => {
581+
const listener = (image: string, status: extensionApi.ProviderConnectionStatus): void => {
582+
if (image === machineInfo.Image && status === 'started') {
583+
listeners.delete(listener);
584+
clearTimeout(timeoutId);
585+
resolve();
586+
}
587+
};
588+
589+
const timeoutId = setTimeout(() => {
590+
listeners.delete(listener);
591+
reject(new Error(`Machine ${name} failed to start within timeout`));
592+
logger?.log(`VM ${name} failed to start within timeout\n`);
593+
}, 60000); // 60 second timeout
594+
595+
listeners.add(listener);
596+
});
597+
598+
const startPromise = startMachine(provider, {
599+
name,
600+
image: machineInfo.Image,
601+
cpus: machineInfo.CPUs,
602+
memory: Number(machineInfo.Memory),
603+
diskSize: Number(machineInfo.DiskSize),
604+
port: machineInfo.Port,
605+
remoteUsername: machineInfo.RemoteUsername,
606+
identityPath: machineInfo.IdentityPath,
607+
vmType: machineInfo.VMType,
608+
});
609+
try {
610+
await Promise.all([startPromise, startedPromise]);
611+
logger?.log('VM started\n');
612+
} catch (error) {
613+
logger?.log(`VM start failed, the VM cannot be registered: ${String(error)}\n`);
614+
throw error;
615+
}
616+
617+
const currentSession = await initAuthentication();
618+
const orgId = currentSession.getOrganizationId();
619+
logger?.log('Registering VM ...\n');
620+
const result = await macadam.executeCommand({
621+
name,
622+
command: 'sudo',
623+
args: ['subscription-manager', 'register', '--force', '--activationkey', 'podman-desktop', '--org', orgId],
624+
runOptions: { logger },
625+
});
626+
if (result.stderr) {
627+
throw new Error(result.stderr);
628+
}
629+
logger?.log('VM registered\n');
550630
}
551631

552632
function updateWSLHyperVEnabledContextValue(value: boolean): void {

0 commit comments

Comments
 (0)