diff --git a/scripts/examples/typescript/.gitignore b/scripts/examples/typescript/.gitignore new file mode 100644 index 00000000000..8377826c063 --- /dev/null +++ b/scripts/examples/typescript/.gitignore @@ -0,0 +1,4 @@ +**/.env +**/node_modules +package-lock.json +dist/ diff --git a/scripts/examples/typescript/Makefile b/scripts/examples/typescript/Makefile new file mode 100644 index 00000000000..30ae67e5afc --- /dev/null +++ b/scripts/examples/typescript/Makefile @@ -0,0 +1,12 @@ +.PHONY: build clean + +build: + sed -i '/version/s/1.0.0/'"$(XAPI_VERSION)"'/' package.json + npm install + npm run build + +publish: + npm publish + +clean: + rm -rf dist/ node-modules/ package-local.json diff --git a/scripts/examples/typescript/README.md b/scripts/examples/typescript/README.md new file mode 100644 index 00000000000..849031682ab --- /dev/null +++ b/scripts/examples/typescript/README.md @@ -0,0 +1,37 @@ +# Typescript bindings for XenAPI +Typescript module for XenAPI, which is inspired by [Python XenAPI](https://xapi-project.github.io/xen-api/usage.html). + +## Usage +The usage of Typescript XenAPI npm library is almost identical to the Python XenAPI library, except it's asynchronous and requires async/await. + +```js +import { xapi_client } from "xen-api-ts"; + +async function main() { + const session = xapi_client(process.env.HOST_URL); + try { + await session.login_with_password(process.env.USERNAME, process.env.PASSWORD); + const hosts = await session.xenapi.host.get_all(); + console.log(hosts); // Do something with the retrieved hosts + } finally { + await session.xenapi.session.logout(); + } +} + +main(); +``` + +More examples can be found in the [tests](tests) folder. +```sh +git clone git@github.com:acefei/xen-api-ts.git +npm install +echo "HOST_URL=xxx" >> .env +echo "USERNAME=xxx" >> .env +echo "PASSWORD=xxx" >> .env +npm test tests/getXapiVersion.ts +``` + +Documentation for all the API classes and their fields can be found in the [XenAPI Reference](https://xapi-project.github.io/xen-api). + +## License +This repository is licensed under the MIT License. diff --git a/scripts/examples/typescript/package.json b/scripts/examples/typescript/package.json new file mode 100644 index 00000000000..83e39ee43ec --- /dev/null +++ b/scripts/examples/typescript/package.json @@ -0,0 +1,32 @@ +{ + "name": "xen-api-lib", + "version": "0.0.1", + "description": "A typescript library for Xen API", + "main": "dist/XenAPI.js", + "types": "dist/XenAPI.d.ts", + "files": [ + "/dist" + ], + "scripts": { + "test": "node -r ts-node/register -r dotenv/config ", + "build": "tsc" + }, + "keywords": [ + "xenserver", + "hypervisor", + "xen-api", + "typescript" + ], + "author": "Su Fei ", + "license": "GPLv2", + "devDependencies": { + "@types/node": "^20.8.9", + "axios": "^1.6.0", + "dotenv": "^16.3.1", + "ts-node": "^10.9.1", + "typescript": "^5.2.2" + }, + "dependencies": { + "json-rpc-2.0": "^1.6.0" + } +} diff --git a/scripts/examples/typescript/src/XenAPI.ts b/scripts/examples/typescript/src/XenAPI.ts new file mode 100644 index 00000000000..d178ab37e3d --- /dev/null +++ b/scripts/examples/typescript/src/XenAPI.ts @@ -0,0 +1,90 @@ +import { JSONRPCClient } from "json-rpc-2.0"; + +class XapiSession { + private client: JSONRPCClient; + + private sessionId: string = ""; + + constructor (hostUrl: string) { + this.client = new JSONRPCClient(async (request) => { + const response = await fetch(`${hostUrl}/jsonrpc`, { + method: "POST", + headers: { + "content-type": "application/json", + "user-agent": "xen api library" + }, + body: JSON.stringify(request) + }); + if (response.status === 200) { + const json = await response.json(); + return this.client.receive(json); + } else if (request.id !== undefined) { + console.error("Connection error with url: ", hostUrl); + return Promise.reject(new Error(response.statusText)); + } + }); + } + + async loginWithPasswd (username: string, password: string): Promise { + this.sessionId = await this.client.request("session.login_with_password", [username, password]); + return this.sessionId; + } + + // we need to save session id into local storage and read it to login xapi after refresh the web browser + async loginWithSession (sessionId: string): Promise { + this.sessionId = ""; + try { + await this.client.request("session.get_all_subject_identifiers", [sessionId]); + this.sessionId = sessionId; + return true; + } catch (error: any) { + if (error?.message === "SESSION_INVALID") { + return false; + } else { + throw error; + } + } + } + + getSessionId (): string { + return this.sessionId; + } + + async xapiRequest (method: string, args: any[] = []): Promise { + try { + return await this.client.request(method, [this.sessionId, ...args]); + } catch (error: any) { + console.error(`Failed to call ${method} with args [${args}] + Error code: ${error?.code} + Error message: ${error?.message} + Error data: ${error?.data}`); + } + } +} + +function xapi_proxy (obj: XapiSession, path: any[] = []): any { + return new Proxy(() => { }, { + get (_target, property) { + return xapi_proxy(obj, path.concat(property)); + }, + apply (_target, _self, args) { + if (path.length > 0) { + if (path[path.length - 1].toLowerCase() == "login_with_password") { + return obj.loginWithPasswd(args[0], args[1]); + } else if (path[path.length - 1].toLowerCase() == "login_with_session") { + return obj.loginWithSession(args[0]); + } else if (path[path.length - 1].toLowerCase() == "get_session_id") { + return obj.getSessionId(); + } else if (path[0].toLowerCase() == "xenapi") { + return obj.xapiRequest(path.slice(1).join("."), args); + } else { + throw new Error(`Method ${path.join(".")} is not supported`); + } + } + } + }); +} + +export function xapi_client (url: string): any { + return xapi_proxy(new XapiSession(url)); +} diff --git a/scripts/examples/typescript/tests/fetchUpdates.ts b/scripts/examples/typescript/tests/fetchUpdates.ts new file mode 100644 index 00000000000..e673b12fe05 --- /dev/null +++ b/scripts/examples/typescript/tests/fetchUpdates.ts @@ -0,0 +1,107 @@ +import { xapi_client } from "../src/XenAPI" +import axios from 'axios' + +async function get_pool_ref(session: any) { + const pool = await session.xenapi.pool.get_all() + return pool[0] +} + +async function set_pool_repos(session: any, pool_ref:string, repo_refs: string[]) { + return await session.xenapi.pool.set_repositories(pool_ref, repo_refs) +} + +async function create_repos(session: any) { + // Ref http://xapi-project.github.io/xen-api/classes/repository.html + const repositories: { [key: string]: any } = { + base: { + name_label: 'base', + name_description: 'base rpm repo', + binary_url: 'https://repo.ops.xenserver.com/xs8/base', + source_url: 'https://repo-src.ops.xenserver.com/xs8/base', + // base repo is not an update repo + update: false, + }, + normal: { + name_label: 'normal', + name_description: 'normal rpm repo', + binary_url: 'https://repo.ops.xenserver.com/xs8/normal', + source_url: 'https://repo-src.ops.xenserver.com/xs8/normal', + update: true, + }, + } + + const res = Object.keys(repositories).sort().map(async repoKey => { + const repo = repositories[repoKey] + const { name_label, name_description, binary_url, source_url, update } = repo + return await session.xenapi.Repository.introduce(name_label, name_description, binary_url, source_url, update) + }) + + const resPromises = await Promise.all(res) + return resPromises +} + +async function get_repos(session: any) { + return await session.xenapi.Repository.get_all() +} + +async function remove_repos(session: any) { + const repo_refs = await get_repos(session) + const res = repo_refs.map(async (ref:string)=> { + return await session.xenapi.Repository.forget(ref) + }) + await Promise.all(res) +} + +async function sync_updates(session: any, pool_ref:string) { + return await session.xenapi.pool.sync_updates(pool_ref, false, '', '') +} + +async function list_updates(session: any, session_id:string) { + try { + const response = await axios.get(`${process.env.HOST_URL}/updates`, { + params: { + session_id: session_id + }, + httpsAgent: { + rejectUnauthorized: false + } + }) + console.log('Response Data:', response.data) + return response.data + } catch (error) { + console.error('Error:', error) + } +} + +async function main() { + console.log(`Login ${process.env.HOST_URL} with ${process.env.USERNAME}`) + const session = xapi_client(process.env.HOST_URL) + const sid = await session.login_with_password(process.env.USERNAME, process.env.PASSWORD) + console.log(`Login successfully with ${sid}`) + + const pool_ref = await get_pool_ref(session) + let repo_refs = await get_repos(session) + if (!repo_refs.length) { + console.log('\nCreate new rpm repos') + repo_refs = await create_repos(session) + } + + console.log('\nSet enabled set of repositories',repo_refs) + await set_pool_repos(session, pool_ref, repo_refs) + + console.log('\nSync updates') + await sync_updates(session, pool_ref) + + console.log('\nList updates') + const updates = await list_updates(session, sid) + console.log(updates) + + console.log('\nClean old rpm repos') + await set_pool_repos(session, pool_ref, []) + await remove_repos(session) + + await session.xenapi.session.logout() + console.log(`\nSession Logout.`) +} + +main() diff --git a/scripts/examples/typescript/tests/getXapiVersion.ts b/scripts/examples/typescript/tests/getXapiVersion.ts new file mode 100644 index 00000000000..e32b159fe69 --- /dev/null +++ b/scripts/examples/typescript/tests/getXapiVersion.ts @@ -0,0 +1,33 @@ +import { xapi_client } from "../src/XenAPI" + +async function get_api_version(session: any) { + const pools = await session.xenapi.pool.get_all_records() + const master_host_ref = pools[Object.keys(pools)[0]].master + const host_record = await session.xenapi.host.get_record(master_host_ref) + return `${host_record.API_version_major}.${host_record.API_version_minor}` +} + +async function main() { + if (process.env.HOST_URL == undefined) { + console.log("Please set HOST_URL in .env first.") + process.exit() + } + + console.log(`Login ${process.env.HOST_URL} with ${process.env.USERNAME}`) + const session = xapi_client(process.env.HOST_URL) + try { + const sid = await session.login_with_password(process.env.USERNAME, process.env.PASSWORD) + console.log(`Login successfully with ${sid}`) + const ver = await get_api_version(session) + console.log(`\nCurrent XAPI Version: ${ver}`) + const hosts = await session.xenapi.host.get_all() + console.log(`\nGet Host list:\n${hosts.join("\n")}`) + } catch (error) { + console.error("An error occurred:", error) + } finally { + await session.xenapi.session.logout() + console.log(`\nSession Logout.`) + } +} + +main() diff --git a/scripts/examples/typescript/tsconfig.json b/scripts/examples/typescript/tsconfig.json new file mode 100644 index 00000000000..15ea3ed0f84 --- /dev/null +++ b/scripts/examples/typescript/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "module": "commonjs", /* Specify what module code is generated. */ + "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + "strict": true, /* Enable all strict type-checking options. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + /* select which files the compiler processes. */ + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules" + ] +}