Skip to content

Commit

Permalink
use dependency injection
Browse files Browse the repository at this point in the history
  • Loading branch information
MathieuRA committed Jan 23, 2025
1 parent e74f705 commit dd52692
Show file tree
Hide file tree
Showing 10 changed files with 276 additions and 162 deletions.
3 changes: 3 additions & 0 deletions @xen-orchestra/rest-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
},
"dependencies": {
"express": "^4.21.2",
"inversify": "^6.2.1",
"inversify-binding-decorators": "^4.0.0",
"reflect-metadata": "^0.2.2",
"swagger-ui-express": "^5.0.1",
"tsoa": "^6.6.0"
}
Expand Down
14 changes: 8 additions & 6 deletions @xen-orchestra/rest-api/src/dashboard/dashboard.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Controller, Get, Route } from 'tsoa'
import * as dashboardService from './dashboard.service.js'
import DashboardService from './dashboard.service.js'
import { inject } from 'inversify'

export type Dashboard = {
vmsStatus: {
Expand All @@ -16,12 +17,13 @@ export type Dashboard = {

@Route('dashboard')
export class DashboardController extends Controller {
#dashboardService
constructor(@inject(DashboardService) dashboardService: DashboardService) {
super()
this.#dashboardService = dashboardService
}
@Get()
public async getDashboard(): Promise<Dashboard> {
const [poolsStatus] = await Promise.all([dashboardService.getPoolsStatus()])
const vmsStatus = dashboardService.getVmsStatus()

const dashboard = { poolsStatus, vmsStatus }
return dashboard
return this.#dashboardService.getDashboard()
}
}
138 changes: 93 additions & 45 deletions @xen-orchestra/rest-api/src/dashboard/dashboard.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,105 @@ import { getRestApi } from '../index.js'
import { XoVm } from '../vms/vm.type.js'
import { Dashboard } from './dashboard.controller.js'

export const getVmsStatus = (): Dashboard['vmsStatus'] => {
const restApi = getRestApi()
const vms = Object.values(restApi.getObjects<XoVm>({ filter: obj => obj.type === 'VM' }))
let running = 0
let inactive = 0
let unknown = 0

vms.forEach(vm => {
if (vm.power_state === 'Running' || vm.power_state === 'Paused') {
running++
} else if (vm.power_state === 'Halted' || vm.power_state === 'Suspended') {
inactive++
} else {
unknown++
}
})

return { running, inactive, unknown }
// cache used to compare changes and trigger dashboard events
type CacheDashboard = {
vmsStatus: Dashboard['vmsStatus']
poolsStatus: Dashboard['poolsStatus']
}
const cache = new Map<keyof CacheDashboard, CacheDashboard[keyof CacheDashboard]>()

export const getPoolsStatus = async (): Promise<Dashboard['poolsStatus']> => {
const restApi = getRestApi()
const servers = await restApi.getServers()
const poolIds = Object.keys(restApi.getObjects({ filter: obj => obj.type === 'pool' }))

let nConnectedServers = 0
let nUnreachableServers = 0
let nUnknownServers = 0
servers.forEach(server => {
// it may happen that some servers are marked as "connected", but no pool matches "server.pool"
// so they are counted as `nUnknownServers`
if (server.status === 'connected' && poolIds.includes(server.poolId)) {
nConnectedServers++
return
}
export default class DashboardService {
#restApi
#fnById = {
// arrow fn to ensure to call the method in the good context
vmsStatus: () => this.#getVmsStatus(),
poolsStatus: () => this.#getPoolsStatus(),
}

if (
server.status === 'disconnected' &&
server.error !== undefined &&
server.error.connectedServerId === undefined
) {
nUnreachableServers++
return
}
constructor() {
this.#restApi = getRestApi()
this.#registerListener()
}

async getDashboard(): Promise<Dashboard> {
const vmsStatus = this.#getVmsStatus()
const poolsStatus = await this.#getPoolsStatus()

return { vmsStatus, poolsStatus }
}

#getVmsStatus(): Dashboard['vmsStatus'] {
const vms = Object.values(this.#restApi.getObjects<XoVm>({ filter: obj => obj.type === 'VM' }))
let running = 0
let inactive = 0
let unknown = 0

vms.forEach(vm => {
if (vm.power_state === 'Running' || vm.power_state === 'Paused') {
running++
} else if (vm.power_state === 'Halted' || vm.power_state === 'Suspended') {
inactive++
} else {
unknown++
}
})

const result = { running, inactive, unknown }
cache.set('vmsStatus', result)

return result
}

async #getPoolsStatus(): Promise<Dashboard['poolsStatus']> {
const servers = await this.#restApi.getServers()
const poolIds = Object.keys(this.#restApi.getObjects({ filter: obj => obj.type === 'pool' }))

let nConnectedServers = 0
let nUnreachableServers = 0
let nUnknownServers = 0
servers.forEach(server => {
// it may happen that some servers are marked as "connected", but no pool matches "server.pool"
// so they are counted as `nUnknownServers`
if (server.status === 'connected' && poolIds.includes(server.poolId)) {
nConnectedServers++
return
}

if (
server.status === 'disconnected' &&
server.error !== undefined &&
server.error.connectedServerId === undefined
) {
nUnreachableServers++
return
}

if (server.status === 'disconnected') {
return
}

nUnknownServers++
})

const result = { connected: nConnectedServers, unreachable: nUnreachableServers, unknown: nUnknownServers }
cache.set('poolsStatus', result)

return result
}

if (server.status === 'disconnected') {
async #maybeSendEvent(key: keyof Dashboard) {
const stringifiedCacheValue = JSON.stringify(cache.get(key))
const newValue = await this.#fnById[key]()
if (JSON.stringify(newValue) === stringifiedCacheValue) {
return
}

nUnknownServers++
})
this.#restApi.sendData(key, 'dashboard', newValue, 'update')
}

return { connected: nConnectedServers, unreachable: nUnreachableServers, unknown: nUnknownServers }
#registerListener() {
this.#restApi.ee.on('vm', () => this.#maybeSendEvent('vmsStatus'))
this.#restApi.ee.on('pool', () => this.#maybeSendEvent('poolsStatus'))
// this.#restApi.ee.on('host', () => this.#maybeSendEvent('hostsStatus'))
}
}
68 changes: 33 additions & 35 deletions @xen-orchestra/rest-api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,34 +5,33 @@ import swaggerOpenApiSpec from './open-api/spec/swagger.json' assert { type: 'js
import { RegisterRoutes } from './open-api/routes/routes.js'
import { XapiXoObject, XoApp } from './xoApp.type.js'
import { EventEmitter } from 'events'
import * as dashboardService from './dashboard/dashboard.service.js'
import DashboardService from './dashboard/dashboard.service.js'
import { iocContainer } from './ioc.js'

class RestApi {
static instance: RestApi | null = null
import 'reflect-metadata'

class RestApi {
#sseClients: Map<symbol, Response> = new Map()

ee = new EventEmitter()

getObjects!: XoApp['getObjects']
getObject!: XoApp['getObject']
getServers!: XoApp['getAllXenServers']
getServer!: XoApp['getXenServer']
getObjects: XoApp['getObjects']
getObject: XoApp['getObject']
getServers: XoApp['getAllXenServers']
getServer: XoApp['getXenServer']

constructor(xoApp: XoApp) {
if (RestApi.instance !== null) {
return RestApi.instance
if (restApi !== undefined) {
throw new Error('RestApi is a singleton')
}

this.#registerListener(xoApp._objects)

// wrap these method with permission
this.getObject = (id, type) => xoApp.getObject(id, type)
this.getObjects = opts => xoApp.getObjects(opts)
this.getServers = () => xoApp.getAllXenServers()
this.getServer = id => xoApp.getXenServer(id)

RestApi.instance = this
this.#registerListener(xoApp._objects)
}

addSseClient(id: symbol, client: Response) {
Expand All @@ -43,32 +42,32 @@ class RestApi {
this.#sseClients.delete(id)
}

#registerListener(event: XoApp['_objects']) {
// XAPI events
event.on('remove', data => {
this.#handleXapiEvent(data, 'remove')
sendData(objId: string, objType: string | undefined, obj: any | undefined, operation: 'update' | 'add' | 'remove') {
this.#sseClients.forEach(client => {
client.write(`data: ${JSON.stringify({ id: objId, type: objType, data: obj, operation })}\n\n`)
})
event.on('add', data => this.#handleXapiEvent(data, 'add'))
event.on('update', data => this.#handleXapiEvent(data, 'update'))
}

// REST API events
this.ee.on('dashboard:vmsStatus', data => {
this.#sendData('vmsStatus', 'dashboard', data, 'update')
})
this.ee.on('dashboard:poolsStatus', data => {
this.#sendData('poolsStatus', 'dashboard', data, 'update')
#registerListener(obj: XoApp['_objects']) {
// XAPI events
obj.on('remove', data => {
this.#handleXapiEvent(data, 'remove')
})
obj.on('add', data => this.#handleXapiEvent(data, 'add'))
obj.on('update', data => this.#handleXapiEvent(data, 'update'))
}

async #handleXapiEvent(data: Record<string, XapiXoObject | undefined>, operation: 'update' | 'add' | 'remove') {
const ids = Object.keys(data)
let vmChanges = false
let poolChanges = false
let hostChanges = false

if (operation === 'remove') {
// on remove operations, we have no way to know the obj type.
vmChanges = true
poolChanges = true
hostChanges = true
}

ids.forEach(id => {
Expand All @@ -85,23 +84,19 @@ class RestApi {
poolChanges = true
}

this.#sendData(id, obj?.type, obj, operation)
this.sendData(id, obj?.type, obj, operation)
})

if (vmChanges) {
const vmsStatus = dashboardService.getVmsStatus()
this.ee.emit('dashboard:vmsStatus', vmsStatus)
this.ee.emit('vm')
}
if (poolChanges) {
const poolsStatus = await dashboardService.getPoolsStatus()
this.ee.emit('dashboard:poolsStatus', poolsStatus)
this.ee.emit('pool')
}
}

#sendData(objId: string, objType: string | undefined, obj: any | undefined, operation: 'update' | 'add' | 'remove') {
this.#sseClients.forEach(client => {
client.write(`data: ${JSON.stringify({ id: objId, type: objType, data: obj, operation })}\n\n`)
})
if (hostChanges) {
this.ee.emit('host')
}
// if we are able to got the object type when remove operation is emit, simply do: `this.ee.emit(obj.type)`
}
}

Expand All @@ -113,4 +108,7 @@ export default function setupRestApi(express: Express, xoApp: XoApp) {

express.use('/rest/v1/api-doc', swaggerUi.serve, swaggerUi.setup(swaggerOpenApiSpec))
RegisterRoutes(express)

// in order to create the instance of the service (and start to listen for dashboard changes)
iocContainer.get(DashboardService)
}
19 changes: 19 additions & 0 deletions @xen-orchestra/rest-api/src/ioc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Container } from 'inversify'
import DashboardService from './dashboard/dashboard.service.js'
import { DashboardController } from './dashboard/dashboard.controller.js'
import { EventsController } from './events/event.controller.js'
import { ServersController } from './servers/server.controller.js'
import { VmsController } from './vms/vm.controller.js'

const iocContainer = new Container()

// all controller need to be added here
// maybe automate here?
// maybe we can use abstract class here do define classique crud?
iocContainer.bind(DashboardService).toSelf().inSingletonScope()
iocContainer.bind(DashboardController).toSelf().inSingletonScope()
iocContainer.bind(ServersController).toSelf().inSingletonScope()
iocContainer.bind(EventsController).toSelf().inSingletonScope()
iocContainer.bind(VmsController).toSelf().inSingletonScope()

export { iocContainer }
11 changes: 7 additions & 4 deletions @xen-orchestra/rest-api/src/servers/server.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@ import { XoServer } from './server.type.js'

@Route('servers')
export class ServersController extends Controller {
#restApi
constructor() {
super()
this.#restApi = getRestApi()
}
@Get()
public getServers(): Promise<XoServer[]> {
const restApi = getRestApi()
return restApi.getServers()
return this.#restApi.getServers()
}

@Get('{id}')
public getServer(@Path() id: XoServer['id']): Promise<XoServer> {
const restApi = getRestApi()
return restApi.getServer(id)
return this.#restApi.getServer(id)
}
}
13 changes: 8 additions & 5 deletions @xen-orchestra/rest-api/src/vms/vm.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,19 @@ import { XoVm } from './vm.type.js'

@Route('vms')
export class VmsController extends Controller {
readonly #type = 'VM'
#restApi
constructor() {
super()
this.#restApi = getRestApi()
}

/**
* Some description
*/
@Get()
public getVms(): XoVm[] {
const restApi = getRestApi()
const vms = restApi.getObjects<XoVm>({
// not working
const vms = this.#restApi.getObjects<XoVm>({
filter: obj => obj.type === 'VM',
})

Expand All @@ -22,7 +26,6 @@ export class VmsController extends Controller {

@Get('{id}')
public getVm(@Path() id: string): XoVm {
const restApi = getRestApi()
return restApi.getObject(id, this.#type)
return this.#restApi.getObject(id, 'VM')
}
}
Loading

0 comments on commit dd52692

Please sign in to comment.