feat: async hypervisor and FIXED vm listing

This commit is contained in:
Eli Bosley
2025-01-28 10:09:29 -05:00
parent f1e53831c8
commit cdfb3c772b
16 changed files with 85 additions and 41 deletions

View File

@@ -34,6 +34,7 @@ if [ ! -d "$source_directory" ]; then
fi fi
fi fi
# Change ownership on copy
# Replace the value inside the rsync command with the user's input # Replace the value inside the rsync command with the user's input
rsync_command="rsync -avz -e ssh $source_directory root@${server_name}:/usr/local/unraid-api" rsync_command="rsync -avz -e ssh $source_directory root@${server_name}:/usr/local/unraid-api"
@@ -44,14 +45,11 @@ echo "$rsync_command"
eval "$rsync_command" eval "$rsync_command"
exit_code=$? exit_code=$?
# Run unraid-api restart on remote host # Chown the directory
dev=${DEV:-true} ssh root@"${server_name}" "chown -R root:root /usr/local/unraid-api"
if [ "$dev" = true ]; then # Run unraid-api restart on remote host
ssh root@"${server_name}" "INTROSPECTION=true unraid-api restart" ssh root@"${server_name}" "INTROSPECTION=true LOG_LEVEL=trace unraid-api restart"
else
ssh root@"${server_name}" "unraid-api restart"
fi
# Play built-in sound based on the operating system # Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then if [[ "$OSTYPE" == "darwin"* ]]; then

View File

@@ -5,25 +5,36 @@ import '@app/dotenv';
import { execa } from 'execa'; import { execa } from 'execa';
import { CommandFactory } from 'nest-commander'; import { CommandFactory } from 'nest-commander';
import { cliLogger, internalLogger } from '@app/core/log'; import { internalLogger, logger } from '@app/core/log';
import { LOG_LEVEL } from '@app/environment';
import { CliModule } from '@app/unraid-api/cli/cli.module'; import { CliModule } from '@app/unraid-api/cli/cli.module';
import { LogService } from '@app/unraid-api/cli/log.service';
const getUnraidApiLocation = async () => {
try {
const shellToUse = await execa('which unraid-api');
if (shellToUse.code !== 0) {
throw new Error('unraid-api not found');
}
return shellToUse.stdout.trim();
} finally {
return '/usr/bin/unraid-api';
}
};
try { try {
const shellToUse = await execa('which unraid-api')
.then((res) => res.toString().trim())
.catch((_) => '/usr/local/bin/unraid-api');
await CommandFactory.run(CliModule, { await CommandFactory.run(CliModule, {
cliName: 'unraid-api', cliName: 'unraid-api',
logger: false, // new LogService(), - enable this to see nest initialization issues logger: LOG_LEVEL === 'TRACE' && new LogService(), // - enable this to see nest initialization issues
completion: { completion: {
fig: true, fig: false,
cmd: 'completion-script', cmd: 'completion-script',
nativeShell: { executablePath: shellToUse }, nativeShell: { executablePath: await getUnraidApiLocation() },
}, },
}); });
process.exit(0); process.exit(0);
} catch (error) { } catch (error) {
cliLogger.error('ERROR:', error); logger.error('ERROR:', error);
internalLogger.error({ internalLogger.error({
message: 'Failed to start unraid-api', message: 'Failed to start unraid-api',
error, error,

View File

@@ -18,9 +18,10 @@ const states = {
* Get vm domains. * Get vm domains.
*/ */
export const getDomains = async () => { export const getDomains = async () => {
const { ConnectListAllDomainsFlags } = await import('@vmngr/libvirt');
const { UnraidHypervisor } = await import('@app/core/utils/vms/get-hypervisor');
try { try {
const { ConnectListAllDomainsFlags } = await import('@vmngr/libvirt');
const { UnraidHypervisor } = await import('@app/core/utils/vms/get-hypervisor');
const hypervisor = await UnraidHypervisor.getInstance().getHypervisor(); const hypervisor = await UnraidHypervisor.getInstance().getHypervisor();
if (!hypervisor) { if (!hypervisor) {
throw new GraphQLError('VMs Disabled'); throw new GraphQLError('VMs Disabled');

View File

@@ -19,7 +19,7 @@ export const BYPASS_PERMISSION_CHECKS = process.env.BYPASS_PERMISSION_CHECKS ===
export const BYPASS_CORS_CHECKS = process.env.BYPASS_CORS_CHECKS === 'true'; export const BYPASS_CORS_CHECKS = process.env.BYPASS_CORS_CHECKS === 'true';
export const LOG_CORS = process.env.LOG_CORS === 'true'; export const LOG_CORS = process.env.LOG_CORS === 'true';
export const LOG_TYPE = (process.env.LOG_TYPE as 'pretty' | 'raw') ?? 'pretty'; export const LOG_TYPE = (process.env.LOG_TYPE as 'pretty' | 'raw') ?? 'pretty';
export const LOG_LEVEL = process.env.LOG_LEVEL as export const LOG_LEVEL = process.env.LOG_LEVEL?.toUpperCase() as
| 'TRACE' | 'TRACE'
| 'DEBUG' | 'DEBUG'
| 'INFO' | 'INFO'

View File

@@ -0,0 +1,34 @@
type Query {
"""Virtual machines"""
vms: Vms
}
type Vms {
id: ID!
domain: [VmDomain!]
}
type Subscription {
vms: Vms
}
# https://libvirt.org/manpages/virsh.html#list
enum VmState {
NOSTATE
RUNNING
IDLE
PAUSED
SHUTDOWN
SHUTOFF
CRASHED
PMSUSPENDED
}
"""A virtual machine"""
type VmDomain {
uuid: ID!
"""A friendly name for the vm"""
name: String
"""Current domain vm state"""
state: VmState!
}

View File

@@ -12,11 +12,13 @@ export class RestartCommand extends CommandRunner {
async run(_): Promise<void> { async run(_): Promise<void> {
try { try {
this.logger.info('Restarting the Unraid API');
const { stderr, stdout } = await execa(PM2_PATH, [ const { stderr, stdout } = await execa(PM2_PATH, [
'restart', 'restart',
ECOSYSTEM_PATH, ECOSYSTEM_PATH,
'--update-env', '--update-env',
]); ]);
this.logger.info('Unraid API restarted');
if (stderr) { if (stderr) {
this.logger.error(stderr); this.logger.error(stderr);
process.exit(1); process.exit(1);

View File

@@ -3,7 +3,7 @@ import { Command, CommandRunner, Option } from 'nest-commander';
import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts'; import { ECOSYSTEM_PATH, PM2_PATH } from '@app/consts';
import { levels, type LogLevel } from '@app/core/log'; import { levels, type LogLevel } from '@app/core/log';
import type { LogService } from '@app/unraid-api/cli/log.service'; import { LogService } from '@app/unraid-api/cli/log.service';
interface StartCommandOptions { interface StartCommandOptions {
'log-level'?: string; 'log-level'?: string;

View File

@@ -58,7 +58,7 @@ const states = {
}, },
}; };
@Resolver() @Resolver('Display')
export class DisplayResolver { export class DisplayResolver {
@Query() @Query()
@UsePermissions({ @UsePermissions({

View File

@@ -5,7 +5,7 @@ import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
import { Resource } from '@app/graphql/generated/api/types'; import { Resource } from '@app/graphql/generated/api/types';
import { getters } from '@app/store/index'; import { getters } from '@app/store/index';
@Resolver() @Resolver('Flash')
export class FlashResolver { export class FlashResolver {
@Query() @Query()
@UsePermissions({ @UsePermissions({

View File

@@ -6,7 +6,7 @@ import type { UserAccount } from '@app/graphql/generated/api/types';
import { Me, Resource } from '@app/graphql/generated/api/types'; import { Me, Resource } from '@app/graphql/generated/api/types';
import { GraphqlUser } from '@app/unraid-api/auth/user.decorator'; import { GraphqlUser } from '@app/unraid-api/auth/user.decorator';
@Resolver() @Resolver('Me')
export class MeResolver { export class MeResolver {
constructor() {} constructor() {}

View File

@@ -4,7 +4,7 @@ import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
import { Resource } from '@app/graphql/generated/api/types'; import { Resource } from '@app/graphql/generated/api/types';
@Resolver() @Resolver('Online')
export class OnlineResolver { export class OnlineResolver {
@Query() @Query()
@UsePermissions({ @UsePermissions({

View File

@@ -6,7 +6,7 @@ import { createSubscription, PUBSUB_CHANNEL } from '@app/core/pubsub';
import { Resource } from '@app/graphql/generated/api/types'; import { Resource } from '@app/graphql/generated/api/types';
import { getters } from '@app/store/index'; import { getters } from '@app/store/index';
@Resolver() @Resolver('Owner')
export class OwnerResolver { export class OwnerResolver {
@Query() @Query()
@UsePermissions({ @UsePermissions({

View File

@@ -9,7 +9,7 @@ import { registrationType, Resource } from '@app/graphql/generated/api/types';
import { getters } from '@app/store/index'; import { getters } from '@app/store/index';
import { FileLoadStatus } from '@app/store/types'; import { FileLoadStatus } from '@app/store/types';
@Resolver() @Resolver('Registration')
export class RegistrationResolver { export class RegistrationResolver {
@Query() @Query()
@UsePermissions({ @UsePermissions({

View File

@@ -7,7 +7,7 @@ import { Resource } from '@app/graphql/generated/api/types';
import { type Server } from '@app/graphql/generated/client/graphql'; import { type Server } from '@app/graphql/generated/client/graphql';
import { getLocalServer } from '@app/graphql/schema/utils'; import { getLocalServer } from '@app/graphql/schema/utils';
@Resolver() @Resolver('Server')
export class ServerResolver { export class ServerResolver {
@Query() @Query()
@UsePermissions({ @UsePermissions({

View File

@@ -5,7 +5,7 @@ import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
import { Resource } from '@app/graphql/generated/api/types'; import { Resource } from '@app/graphql/generated/api/types';
import { getters } from '@app/store/index'; import { getters } from '@app/store/index';
@Resolver() @Resolver('Vars')
export class VarsResolver { export class VarsResolver {
@Query() @Query()
@UsePermissions({ @UsePermissions({

View File

@@ -1,11 +1,10 @@
import { Query, Resolver } from '@nestjs/graphql'; import { Query, ResolveField, Resolver } from '@nestjs/graphql';
import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz'; import { AuthActionVerb, AuthPossession, UsePermissions } from 'nest-authz';
import { getDomains } from '@app/core/modules/vms/get-domains'; import { Resource, type VmDomain } from '@app/graphql/generated/api/types';
import { Resource } from '@app/graphql/generated/api/types';
@Resolver() @Resolver('Vms')
export class VmsResolver { export class VmsResolver {
@Query() @Query()
@UsePermissions({ @UsePermissions({
@@ -14,17 +13,16 @@ export class VmsResolver {
possession: AuthPossession.ANY, possession: AuthPossession.ANY,
}) })
public async vms() { public async vms() {
return {}; console.log('Resolving Domains');
return {
id: 'vms',
};
} }
@Resolver('domain') @ResolveField('domain')
@Query() public async domain(): Promise<Array<VmDomain>> {
@UsePermissions({ const { getDomains } = await import('@app/core/modules/vms/get-domains');
action: AuthActionVerb.READ, const domains = await getDomains();
resource: 'vms/domain', return domains;
possession: AuthPossession.ANY,
})
public async domain() {
return getDomains();
} }
} }