-
Notifications
You must be signed in to change notification settings - Fork 292
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
**/.env | ||
**/node_modules | ||
package-lock.json | ||
dist/ |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. futhermore, in for instance, your expanded build could be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 |
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"; | ||
acefei marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you have a different license between here and the There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
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", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
], | ||
acefei marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"author": "Su Fei <fei.su@cloud.com>", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { JSONRPCClient } from "json-rpc-2.0"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} |
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you're not really using typescript throughout this. Also in |
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 This is unorthodox and not immediately clear to consumers of the npm. They should be under the If they're not tests but examples, they should be in |
||
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() | ||
acefei marked this conversation as resolved.
Show resolved
Hide resolved
|
||
console.log(`\nSession Logout.`) | ||
} | ||
|
||
main() |
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() { | ||
acefei marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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() |
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
"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" | ||
] | ||
} |
There was a problem hiding this comment.
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?