Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions api/scripts/deploy-dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ if [ ! -d "$source_directory" ]; then
fi
fi

# Change ownership on copy
# 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"

Expand All @@ -44,14 +45,11 @@ echo "$rsync_command"
eval "$rsync_command"
exit_code=$?

# Run unraid-api restart on remote host
dev=${DEV:-true}
# Chown the directory
ssh root@"${server_name}" "chown -R root:root /usr/local/unraid-api"

if [ "$dev" = true ]; then
ssh root@"${server_name}" "INTROSPECTION=true unraid-api restart"
else
ssh root@"${server_name}" "unraid-api restart"
fi
# Run unraid-api restart on remote host
ssh root@"${server_name}" "INTROSPECTION=true LOG_LEVEL=trace unraid-api restart"

# Play built-in sound based on the operating system
if [[ "$OSTYPE" == "darwin"* ]]; then
Expand Down
26 changes: 18 additions & 8 deletions api/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,35 @@ import '@app/dotenv';
import { execa } from 'execa';
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 { LogService } from '@app/unraid-api/cli/log.service';

const getUnraidApiLocation = async () => {
try {
const shellToUse = await execa('which unraid-api');
return shellToUse.stdout.trim();
} catch (err) {
logger.debug('Could not find unraid-api in PATH, using default location');

return '/usr/bin/unraid-api';
}
};

try {
const shellToUse = await execa('which unraid-api')
.then((res) => res.toString().trim())
.catch((_) => '/usr/local/bin/unraid-api');
await CommandFactory.run(CliModule, {
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: {
fig: true,
fig: false,
cmd: 'completion-script',
nativeShell: { executablePath: shellToUse },
nativeShell: { executablePath: await getUnraidApiLocation() },
},
});
process.exit(0);
} catch (error) {
cliLogger.error('ERROR:', error);
logger.error('ERROR:', error);
internalLogger.error({
message: 'Failed to start unraid-api',
error,
Expand Down
24 changes: 13 additions & 11 deletions api/src/core/modules/vms/get-domains.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { ConnectListAllDomainsFlags } from '@vmngr/libvirt';
import { getHypervisor } from '@app/core/utils/vms/get-hypervisor';
import { VmState, type VmDomain } from '@app/graphql/generated/api/types';
import { GraphQLError } from 'graphql';

import type { VmDomain } from '@app/graphql/generated/api/types';
import { VmState } from '@app/graphql/generated/api/types';

const states = {
0: 'NOSTATE',
1: 'RUNNING',
Expand All @@ -17,10 +17,12 @@ const states = {
/**
* Get vm domains.
*/
export const getDomains =async () => {

export const getDomains = async () => {
try {
const hypervisor = await getHypervisor();
const { ConnectListAllDomainsFlags } = await import('@vmngr/libvirt');
const { UnraidHypervisor } = await import('@app/core/utils/vms/get-hypervisor');

const hypervisor = await UnraidHypervisor.getInstance().getHypervisor();
if (!hypervisor) {
throw new GraphQLError('VMs Disabled');
}
Expand All @@ -30,9 +32,7 @@ export const getDomains =async () => {
);

const autoStartDomainNames = await Promise.all(
autoStartDomains.map(async (domain) =>
hypervisor.domainGetName(domain)
)
autoStartDomains.map(async (domain) => hypervisor.domainGetName(domain))
);

// Get all domains
Expand All @@ -53,9 +53,11 @@ export const getDomains =async () => {
})
);

return resolvedDomains;
return resolvedDomains;
} catch (error: unknown) {
// If we hit an error expect libvirt to be offline
throw new GraphQLError(`Failed to fetch domains with error: ${error instanceof Error ? error.message : 'Unknown Error'}`);
throw new GraphQLError(
`Failed to fetch domains with error: ${error instanceof Error ? error.message : 'Unknown Error'}`
);
}
};
64 changes: 37 additions & 27 deletions api/src/core/utils/vms/get-hypervisor.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { access } from 'fs/promises';
import { constants } from 'fs';
import { access } from 'fs/promises';

import type { Hypervisor as HypervisorType } from '@vmngr/libvirt';

import { Hypervisor } from '@vmngr/libvirt';
import { libvirtLogger } from '@app/core/log';

const uri = process.env.LIBVIRT_URI ?? 'qemu:///system';

let hypervisor: Hypervisor | null;

const libvirtPid = '/var/run/libvirt/libvirtd.pid';

const isLibvirtRunning = async (): Promise<boolean> => {
Expand All @@ -19,29 +18,40 @@ const isLibvirtRunning = async (): Promise<boolean> => {
}
};

export const getHypervisor = async (): Promise<Hypervisor> => {
// Return hypervisor if it's already connected
const running = await isLibvirtRunning();
export class UnraidHypervisor {
private static instance: UnraidHypervisor | null = null;
private hypervisor: HypervisorType | null = null;
private constructor() {}

if (hypervisor && running) {
return hypervisor;
public static getInstance(): UnraidHypervisor {
if (this.instance === null) {
this.instance = new UnraidHypervisor();
}
return this.instance;
}

if (!running) {
hypervisor = null;
throw new Error('Libvirt is not running');
}

hypervisor = new Hypervisor({ uri });
await hypervisor.connectOpen().catch((error: unknown) => {
libvirtLogger.error(
`Failed starting VM hypervisor connection with "${
(error as Error).message
}"`
);

throw error;
});

return hypervisor;
};
public async getHypervisor(): Promise<HypervisorType | null> {
// Return hypervisor if it's already connected
const running = await isLibvirtRunning();

if (this.hypervisor && running) {
return this.hypervisor;
}

if (!running) {
this.hypervisor = null;
throw new Error('Libvirt is not running');
}
const { Hypervisor } = await import('@vmngr/libvirt');
this.hypervisor = new Hypervisor({ uri });
await this.hypervisor.connectOpen().catch((error: unknown) => {
libvirtLogger.error(
`Failed starting VM hypervisor connection with "${(error as Error).message}"`
);

throw error;
});

return this.hypervisor;
}
}
93 changes: 47 additions & 46 deletions api/src/core/utils/vms/parse-domain.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { type Domain } from '@app/core/types';
import { getHypervisor } from '@app/core/utils/vms/get-hypervisor';

export type DomainLookupType = 'id' | 'uuid' | 'name';

Expand All @@ -11,49 +10,51 @@ export type DomainLookupType = 'id' | 'uuid' | 'name';
* @private
*/
export const parseDomain = async (type: DomainLookupType, id: string): Promise<Domain> => {
const types = {
id: 'lookupDomainByIdAsync',
uuid: 'lookupDomainByUUIDAsync',
name: 'lookupDomainByNameAsync',
};

if (!type || !Object.keys(types).includes(type)) {
throw new Error(`Type must be one of [${Object.keys(types).join(', ')}], ${type} given.`);
}

const client = await getHypervisor();
const method = types[type];
const domain = await client[method](id);
const info = await domain.getInfoAsync();

const [uuid, osType, autostart, maxMemory, schedulerType, schedulerParameters, securityLabel, name] = await Promise.all([
domain.getUUIDAsync(),
domain.getOSTypeAsync(),
domain.getAutostartAsync(),
domain.getMaxMemoryAsync(),
domain.getSchedulerTypeAsync(),
domain.getSchedulerParametersAsync(),
domain.getSecurityLabelAsync(),
domain.getNameAsync(),
]);

const results = {
uuid,
osType,
autostart,
maxMemory,
schedulerType,
schedulerParameters,
securityLabel,
name,
...info,
state: info.state.replace(' ', '_'),
};

if (info.state === 'running') {
results.vcpus = await domain.getVcpusAsync();
results.memoryStats = await domain.getMemoryStatsAsync();
}

return results;
const types = {
id: 'lookupDomainByIdAsync',
uuid: 'lookupDomainByUUIDAsync',
name: 'lookupDomainByNameAsync',
};

if (!type || !Object.keys(types).includes(type)) {
throw new Error(`Type must be one of [${Object.keys(types).join(', ')}], ${type} given.`);
}

const { UnraidHypervisor } = await import('@app/core/utils/vms/get-hypervisor');
const client = await UnraidHypervisor.getInstance().getHypervisor();
const method = types[type];
const domain = await client[method](id);
const info = await domain.getInfoAsync();

const [uuid, osType, autostart, maxMemory, schedulerType, schedulerParameters, securityLabel, name] =
await Promise.all([
domain.getUUIDAsync(),
domain.getOSTypeAsync(),
domain.getAutostartAsync(),
domain.getMaxMemoryAsync(),
domain.getSchedulerTypeAsync(),
domain.getSchedulerParametersAsync(),
domain.getSecurityLabelAsync(),
domain.getNameAsync(),
]);

const results = {
uuid,
osType,
autostart,
maxMemory,
schedulerType,
schedulerParameters,
securityLabel,
name,
...info,
state: info.state.replace(' ', '_'),
};

if (info.state === 'running') {
results.vcpus = await domain.getVcpusAsync();
results.memoryStats = await domain.getMemoryStatsAsync();
}

return results;
};
2 changes: 1 addition & 1 deletion api/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 LOG_CORS = process.env.LOG_CORS === 'true';
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'
| 'DEBUG'
| 'INFO'
Expand Down
12 changes: 6 additions & 6 deletions api/src/graphql/generated/api/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,8 @@ export function AccessUrlInputSchema(): z.ZodObject<Properties<AccessUrlInput>>

export function AddPermissionInputSchema(): z.ZodObject<Properties<AddPermissionInput>> {
return z.object({
action: z.string(),
possession: z.string(),
resource: ResourceSchema,
role: RoleSchema
actions: z.array(z.string()),
resource: ResourceSchema
})
}

Expand Down Expand Up @@ -326,7 +324,8 @@ export function CreateApiKeyInputSchema(): z.ZodObject<Properties<CreateApiKeyIn
return z.object({
description: z.string().nullish(),
name: z.string(),
roles: z.array(RoleSchema)
permissions: z.array(z.lazy(() => AddPermissionInputSchema())).nullish(),
roles: z.array(RoleSchema).nullish()
})
}

Expand Down Expand Up @@ -1230,7 +1229,8 @@ export function VmDomainSchema(): z.ZodObject<Properties<VmDomain>> {
export function VmsSchema(): z.ZodObject<Properties<Vms>> {
return z.object({
__typename: z.literal('Vms').optional(),
domain: z.array(VmDomainSchema()).nullish()
domain: z.array(VmDomainSchema()).nullish(),
id: z.string()
})
}

Expand Down
9 changes: 5 additions & 4 deletions api/src/graphql/generated/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,8 @@ export type AccessUrlInput = {
};

export type AddPermissionInput = {
action: Scalars['String']['input'];
possession: Scalars['String']['input'];
actions: Array<Scalars['String']['input']>;
resource: Resource;
role: Role;
};

export type AddRoleForApiKeyInput = {
Expand Down Expand Up @@ -350,7 +348,8 @@ export enum ContainerState {
export type CreateApiKeyInput = {
description?: InputMaybe<Scalars['String']['input']>;
name: Scalars['String']['input'];
roles: Array<Role>;
permissions?: InputMaybe<Array<AddPermissionInput>>;
roles?: InputMaybe<Array<Role>>;
};

export type Devices = {
Expand Down Expand Up @@ -1682,6 +1681,7 @@ export enum VmState {
export type Vms = {
__typename?: 'Vms';
domain?: Maybe<Array<VmDomain>>;
id: Scalars['ID']['output'];
};

export enum WAN_ACCESS_TYPE {
Expand Down Expand Up @@ -3090,6 +3090,7 @@ export type VmDomainResolvers<ContextType = Context, ParentType extends Resolver

export type VmsResolvers<ContextType = Context, ParentType extends ResolversParentTypes['Vms'] = ResolversParentTypes['Vms']> = ResolversObject<{
domain?: Resolver<Maybe<Array<ResolversTypes['VmDomain']>>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
}>;

Expand Down
Loading
Loading