Skip to content

CP-44477 Add Typescript bindings for XenAPI #5623

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
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
4 changes: 4 additions & 0 deletions scripts/examples/typescript/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
**/.env
**/node_modules
package-lock.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we not adding package-lock? don't we want a specific dependency list?

dist/
12 changes: 12 additions & 0 deletions scripts/examples/typescript/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.PHONY: build clean

build:
sed -i '/version/s/1.0.0/'"$(XAPI_VERSION)"'/' package.json
npm install
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be

npm ci --omit=dev

npm run build

publish:
npm publish
Comment on lines +8 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

honestly don't think we'd need this. why are we adding a publish here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

futhermore, in npm you'd usually use scripts in the package.json, not Makefiles. I'd move everything there if you need it

for instance, your expanded build could be build:production under scripts

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the publish might be useful if we want to publish automatically to npm like we do for XenAPI.py on pypi?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the publish might be useful if we want to publish automatically to npm like we do for XenAPI.py on pypi?

Yes I agree but we're not publishing right now. Unless that's planned with this PR, in which case we need GitHub actions, and either a publishing setup on GitHub or npmjs.com.

Also for publishing I'd argue we'd need to set up a XenServer account. Let's do it properly. For instance I don't see why we have this already: https://www.npmjs.com/package/xen-api-ts


clean:
rm -rf dist/ node-modules/ package-local.json
37 changes: 37 additions & 0 deletions scripts/examples/typescript/README.md
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you have a different license between here and the package.json

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any objections to using BSD2 like the rest of the SDK?

32 changes: 32 additions & 0 deletions scripts/examples/typescript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "xen-api-lib",
"version": "0.0.1",
"description": "A typescript library for Xen API",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a little more descriptive wouldn't hurt. let's tell our users this isn't an SDK but some basic bindings to connect to XenServer

Copy link
Contributor

@kc284 kc284 May 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact it's not bindings either since we don't expose/bind any classes/fields/calls to the respective language concepts. Library or module sounds ok to me, but I agree that we can offer a bit more detail, like, as you said, that one can connect to XS and manage resources, pools, servers, VMs, SRs etc.

"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 <fei.su@cloud.com>",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what the expected value here is but we have CSG for all SDKs rather than individual authors. maybe let's confirm what's needed here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think individual authors used to be a past practice and at some point we started replacing them with the xapi project email address (not sure if there are left overs here and there).

"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"
}
}
90 changes: 90 additions & 0 deletions scripts/examples/typescript/src/XenAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { JSONRPCClient } from "json-rpc-2.0";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're missing copyright notices on these files. I'm guessing that's needed for these?


class XapiSession {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there's no docs all throughout this module. let's add some for users of our client

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<string> {
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<boolean> {
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<any> {
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));
}
107 changes: 107 additions & 0 deletions scripts/examples/typescript/tests/fetchUpdates.ts
Original file line number Diff line number Diff line change
@@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're not really using typescript throughout this. Session should be a type and you should minimise (if not eliminate) any use of any

Also in XenAPI.ts you're using any when you don't really need to

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)
Comment on lines +77 to +79
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate what you're doing here but if I have to be nit picky this is not how you would test in ts

I'd use jester, add it as a dev dependency, create test files, and then make them use .env to run.

This is unorthodox and not immediately clear to consumers of the npm.

They should be under the typescript folder, and that will allow you to control them all from there

If they're not tests but examples, they should be in xenserver-samples

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()
33 changes: 33 additions & 0 deletions scripts/examples/typescript/tests/getXapiVersion.ts
Original file line number Diff line number Diff line change
@@ -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()
19 changes: 19 additions & 0 deletions scripts/examples/typescript/tsconfig.json
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is the first time that you're adding this library, please also add eslint so we can keep this codebase clean :)

Original file line number Diff line number Diff line change
@@ -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. */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't looked at all the packages you're importing but can't we use esm?

"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"
]
}
Loading