mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
Compare commits
9 Commits
326c152fac
...
feat_graph
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb110af59b | ||
|
|
08166f9a18 | ||
|
|
544d071288 | ||
|
|
bce26a910c | ||
|
|
f5a2de7fec | ||
|
|
60d76e6b8e | ||
|
|
a28ad1ddeb | ||
|
|
45bfd53a40 | ||
|
|
d6f5c8890e |
@@ -78,6 +78,10 @@
|
||||
"fs-extra": "^11.1.0",
|
||||
"get-stream": "^7.0.1",
|
||||
"golike-defer": "^0.5.1",
|
||||
"graphql": "^15.8.0",
|
||||
"graphql-http": "^1.22.0",
|
||||
"graphql-subscriptions": "^2.0.0",
|
||||
"graphql-ws": "^5.15.0",
|
||||
"hashy": "^0.11.1",
|
||||
"helmet": "^3.9.0",
|
||||
"highland": "^2.11.1",
|
||||
@@ -115,6 +119,7 @@
|
||||
"pw": "^0.0.4",
|
||||
"readable-stream": "^4.1.0",
|
||||
"redis": "^4.2.0",
|
||||
"ruru": "^2.0.0-beta.11",
|
||||
"semver": "^7.3.2",
|
||||
"serve-static": "^1.13.1",
|
||||
"set-cookie-parser": "^2.3.5",
|
||||
@@ -123,6 +128,7 @@
|
||||
"split2": "^4.1.0",
|
||||
"stoppable": "^1.0.5",
|
||||
"subleveldown": "^6.0.1",
|
||||
"subscriptions-transport-ws": "^0.11.0",
|
||||
"syslog-client": "^1.1.1",
|
||||
"tar-stream": "^3.1.6",
|
||||
"tmp": "^0.2.1",
|
||||
|
||||
86
packages/xo-server/schema.graphql
Normal file
86
packages/xo-server/schema.graphql
Normal file
@@ -0,0 +1,86 @@
|
||||
schema {
|
||||
query: XoQueryType
|
||||
subscription: RootSubscriptionType
|
||||
}
|
||||
|
||||
type XoQueryType {
|
||||
host(id: ID): host
|
||||
hosts(limit: Int, offset: Int, filter: String): [host]
|
||||
vm(id: ID): VM
|
||||
vms(limit: Int, offset: Int, filter: String): [VM]
|
||||
vbd(id: ID): VBD
|
||||
vbds(limit: Int, offset: Int, filter: String): [VBD]
|
||||
vdi(id: ID): VDI
|
||||
vdis(limit: Int, offset: Int, filter: String): [VDI]
|
||||
sr(id: ID): SR
|
||||
srs(limit: Int, offset: Int, filter: String): [SR]
|
||||
}
|
||||
|
||||
type host {
|
||||
id: ID
|
||||
name_label: String
|
||||
description: String
|
||||
timestamp: Int
|
||||
vms(limit: Int, offset: Int, filter: String): [VM]
|
||||
}
|
||||
|
||||
type VM {
|
||||
id: ID
|
||||
name_label: String
|
||||
description: String
|
||||
power_state: String
|
||||
timestamp: Int
|
||||
host(filter: String): host
|
||||
vbds(limit: Int, offset: Int, filter: String): [VBD]
|
||||
}
|
||||
|
||||
type VBD {
|
||||
attached: Boolean
|
||||
bootable: Boolean
|
||||
is_cd_drive: Boolean
|
||||
read_only: Boolean
|
||||
device: String
|
||||
position: String
|
||||
timestamp: Int
|
||||
vm(filter: String): VM
|
||||
vdi(filter: String): VDI
|
||||
}
|
||||
|
||||
type VDI {
|
||||
id: ID
|
||||
name_label: String
|
||||
description: String
|
||||
size: Int
|
||||
usage: Int
|
||||
timestamp: Int
|
||||
vbds(limit: Int, offset: Int, filter: String): [VBD]
|
||||
sr(filter: String): SR
|
||||
}
|
||||
|
||||
type SR {
|
||||
id: ID
|
||||
name_label: String
|
||||
description: String
|
||||
size: Int
|
||||
usage: Int
|
||||
timestamp: Int
|
||||
vdis(limit: Int, offset: Int, filter: String): [VDI]
|
||||
}
|
||||
|
||||
type RootSubscriptionType {
|
||||
hostUpdated: host
|
||||
hostRemoved: host
|
||||
hostAdded: host
|
||||
vmUpdated: VM
|
||||
vmRemoved: VM
|
||||
vmAdded: VM
|
||||
vbdUpdated: VBD
|
||||
vbdRemoved: VBD
|
||||
vbdAdded: VBD
|
||||
vdiUpdated: VDI
|
||||
vdiRemoved: VDI
|
||||
vdiAdded: VDI
|
||||
srUpdated: SR
|
||||
srRemoved: SR
|
||||
srAdded: SR
|
||||
}
|
||||
@@ -273,7 +273,7 @@ async function setUpPassport(express, xo, { authentication: authCfg, http: { coo
|
||||
}
|
||||
|
||||
const SIGNIN_STRATEGY_RE = /^\/signin\/([^/]+)(\/callback)?(:?\?.*)?$/
|
||||
const UNCHECKED_URL_RE = /(?:^\/rest\/)|favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/
|
||||
const UNCHECKED_URL_RE = /(?:^\/rest\/)|(?:^\/graphql\/)|favicon|fontawesome|images|styles|\.(?:css|jpg|png)$/
|
||||
express.use(async (req, res, next) => {
|
||||
const { url } = req
|
||||
|
||||
|
||||
28
packages/xo-server/src/xo-mixins/graphql/index.mjs
Normal file
28
packages/xo-server/src/xo-mixins/graphql/index.mjs
Normal file
@@ -0,0 +1,28 @@
|
||||
import bodyParser from 'body-parser'
|
||||
import { GraphQLSchema, printSchema } from 'graphql'
|
||||
|
||||
import XapiGraphQlBackend from './schema.mjs'
|
||||
import { createHandler } from 'graphql-http/lib/use/express'
|
||||
import { useServer } from 'graphql-ws/lib/use/ws'
|
||||
import { WebSocketServer } from 'ws' // yarn add ws
|
||||
import fs from 'node:fs/promises'
|
||||
|
||||
export default class GraphQlApi {
|
||||
constructor(app, { express, ...other }) {
|
||||
if (express === undefined) {
|
||||
return
|
||||
}
|
||||
// console.log(Object.keys(other))
|
||||
const schema = new GraphQLSchema(new XapiGraphQlBackend(app))
|
||||
const webSocketServer = new WebSocketServer({
|
||||
server: express,
|
||||
path: '/graphql/v0',
|
||||
})
|
||||
|
||||
useServer({ schema }, webSocketServer)
|
||||
fs.writeFile('./schema.graphql', printSchema(schema)).catch(console.error)
|
||||
const handler = createHandler({ schema })
|
||||
express.use('/graphql/v0', bodyParser.json())
|
||||
express.all('/graphql/v0', handler)
|
||||
}
|
||||
}
|
||||
254
packages/xo-server/src/xo-mixins/graphql/schema.mjs
Normal file
254
packages/xo-server/src/xo-mixins/graphql/schema.mjs
Normal file
@@ -0,0 +1,254 @@
|
||||
// schema.js
|
||||
import { GraphQLObjectType, GraphQLString, GraphQLID, GraphQLList, GraphQLInt, GraphQLBoolean } from 'graphql'
|
||||
import { PubSub } from 'graphql-subscriptions'
|
||||
import * as CM from 'complex-matcher'
|
||||
import { every } from '@vates/predicates'
|
||||
import { ifDef } from '@xen-orchestra/defined'
|
||||
|
||||
const pubsub = new PubSub()
|
||||
|
||||
const handleOptionalUserFilter = filter => filter && CM.parse(filter).createPredicate()
|
||||
|
||||
export default class XapiGraphQlSchema {
|
||||
#app
|
||||
#query
|
||||
#subscription
|
||||
#types
|
||||
get query() {
|
||||
return this.#query
|
||||
}
|
||||
get subscription() {
|
||||
return this.#subscription
|
||||
}
|
||||
constructor(app) {
|
||||
this.#app = app
|
||||
app.objects.on('add', items => {
|
||||
Object.values(items).forEach(item => {
|
||||
pubsub.publish(`${item.type.toUpperCase()}_ADDED`, item)
|
||||
})
|
||||
})
|
||||
app.objects.on('update', items => {
|
||||
Object.values(items).forEach(item => {
|
||||
pubsub.publish(`${item.type.toUpperCase()}_UPDATED`, item)
|
||||
})
|
||||
})
|
||||
app.objects.on('remove', items => {
|
||||
Object.keys(items).forEach(id => {
|
||||
pubsub.publish(`REMOVED`, { id })
|
||||
})
|
||||
})
|
||||
this.#makeTypes()
|
||||
this.#makeQueries()
|
||||
this.#makeSubscription()
|
||||
}
|
||||
#resolveXapiObject(id) {
|
||||
return { ...this.#app.getObject(id), timestamp: Math.round(Date.now() / 1000) }
|
||||
}
|
||||
|
||||
#resolveXapiObjects(xapiType, { filter, limit, offset } = {}) {
|
||||
return Object.values(
|
||||
this.#app.getObjects({
|
||||
filter: every(_ => _.type === xapiType, handleOptionalUserFilter(filter)),
|
||||
limit: ifDef(limit, Number),
|
||||
offset: ifDef(offset, Number),
|
||||
})
|
||||
).map(o => ({ ...o, timestamp: Math.round(Date.now() / 1000) }))
|
||||
}
|
||||
|
||||
#makeTypes() {
|
||||
this.#types = {}
|
||||
const self = this
|
||||
|
||||
const standardCollectionArgs = {
|
||||
limit: { type: GraphQLInt },
|
||||
offset: { type: GraphQLInt },
|
||||
filter: { type: GraphQLString },
|
||||
}
|
||||
|
||||
this.#types.host = new GraphQLObjectType({
|
||||
name: 'host',
|
||||
fields: () => ({
|
||||
id: { type: GraphQLID },
|
||||
name_label: { type: GraphQLString },
|
||||
description: { type: GraphQLString },
|
||||
timestamp: { type: GraphQLInt },
|
||||
vms: {
|
||||
type: GraphQLList(self.#types.VM),
|
||||
args: standardCollectionArgs,
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObjects('VM', args).filter(vm => vm.$container === parent.id)
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
this.#types.VM = new GraphQLObjectType({
|
||||
name: 'VM',
|
||||
fields: () => ({
|
||||
id: { type: GraphQLID },
|
||||
name_label: { type: GraphQLString },
|
||||
description: { type: GraphQLString },
|
||||
power_state: { type: GraphQLString },
|
||||
timestamp: { type: GraphQLInt },
|
||||
host: {
|
||||
type: self.#types.host,
|
||||
args: {
|
||||
filter: { type: GraphQLString },
|
||||
},
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObject(parent.$container)
|
||||
},
|
||||
},
|
||||
vbds: {
|
||||
type: GraphQLList(self.#types.VBD),
|
||||
args: standardCollectionArgs,
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObjects('VBD', args).filter(vbd => vbd.VM === parent.id)
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
this.#types.VBD = new GraphQLObjectType({
|
||||
name: 'VBD',
|
||||
fields: () => ({
|
||||
attached: { type: GraphQLBoolean },
|
||||
bootable: { type: GraphQLBoolean },
|
||||
is_cd_drive: { type: GraphQLBoolean },
|
||||
read_only: { type: GraphQLBoolean },
|
||||
device: { type: GraphQLString },
|
||||
position: { type: GraphQLString },
|
||||
timestamp: { type: GraphQLInt },
|
||||
vm: {
|
||||
type: self.#types.VM,
|
||||
args: {
|
||||
filter: { type: GraphQLString },
|
||||
},
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObject(parent.VM)
|
||||
},
|
||||
},
|
||||
vdi: {
|
||||
type: self.#types.VDI,
|
||||
args: {
|
||||
filter: { type: GraphQLString },
|
||||
},
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObject(parent.VDI)
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
this.#types.VDI = new GraphQLObjectType({
|
||||
name: 'VDI',
|
||||
fields: () => ({
|
||||
id: { type: GraphQLID },
|
||||
name_label: { type: GraphQLString },
|
||||
description: { type: GraphQLString },
|
||||
size: { type: GraphQLInt },
|
||||
usage: { type: GraphQLInt },
|
||||
timestamp: { type: GraphQLInt },
|
||||
|
||||
vbds: {
|
||||
type: GraphQLList(self.#types.VBD),
|
||||
args: standardCollectionArgs,
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObjects('VBD', args).filter(vbd => vbd.VDI === parent.id)
|
||||
},
|
||||
},
|
||||
sr: {
|
||||
type: self.#types.SR,
|
||||
args: {
|
||||
filter: { type: GraphQLString },
|
||||
},
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObject(parent?.$SR)
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
this.#types.SR = new GraphQLObjectType({
|
||||
name: 'SR',
|
||||
fields: () => ({
|
||||
id: { type: GraphQLID },
|
||||
name_label: { type: GraphQLString },
|
||||
description: { type: GraphQLString },
|
||||
size: { type: GraphQLInt },
|
||||
usage: { type: GraphQLInt },
|
||||
timestamp: { type: GraphQLInt },
|
||||
|
||||
vdis: {
|
||||
type: GraphQLList(self.#types.VDI),
|
||||
args: standardCollectionArgs,
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObjects('VDI', args).filter(vdi => vdi.SR === parent.id)
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
#makeQueries() {
|
||||
const self = this
|
||||
const fields = {}
|
||||
Object.entries(this.#types).forEach(([typeName, type]) => {
|
||||
fields[typeName.toLocaleLowerCase()] = {
|
||||
type,
|
||||
args: {
|
||||
id: { type: GraphQLID },
|
||||
},
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObject(args.id)
|
||||
},
|
||||
}
|
||||
fields[typeName.toLocaleLowerCase() + 's'] = {
|
||||
type: new GraphQLList(type),
|
||||
args: {
|
||||
limit: { type: GraphQLInt },
|
||||
offset: { type: GraphQLInt },
|
||||
filter: { type: GraphQLString },
|
||||
},
|
||||
resolve(parent, args) {
|
||||
return self.#resolveXapiObjects(typeName, args)
|
||||
},
|
||||
}
|
||||
})
|
||||
this.#query = new GraphQLObjectType({
|
||||
name: 'XoQueryType',
|
||||
fields,
|
||||
})
|
||||
}
|
||||
|
||||
#makeSubscription() {
|
||||
const fields = {}
|
||||
Object.entries(this.#types).forEach(([typeName, type]) => {
|
||||
fields[`${typeName.toLocaleLowerCase()}Updated`] = {
|
||||
type,
|
||||
subscribe: (parent, args) => {
|
||||
return pubsub.asyncIterator(`${typeName.toUpperCase()}_UPDATED`)
|
||||
},
|
||||
resolve: payload => payload,
|
||||
}
|
||||
fields[`${typeName.toLocaleLowerCase()}Removed`] = {
|
||||
type,
|
||||
// args: { id: { type: new GraphQLNonNull(GraphQLID) } },
|
||||
subscribe: (parent, args) => {
|
||||
return pubsub.asyncIterator(`${typeName.toUpperCase()}_REMOVED`)
|
||||
},
|
||||
resolve: payload => payload,
|
||||
}
|
||||
fields[typeName.toLocaleLowerCase() + 'Added'] = {
|
||||
type: type,
|
||||
subscribe(parent, args) {
|
||||
return pubsub.asyncIterator(`${typeName.toUpperCase()}_ADDED`)
|
||||
},
|
||||
resolve: payload => payload,
|
||||
}
|
||||
})
|
||||
this.#subscription = new GraphQLObjectType({
|
||||
name: 'RootSubscriptionType',
|
||||
fields,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user