diff --git a/README.md b/README.md index 8f9f330..26bbda1 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Control and use data from your Proxmox VE ### **WORK IN PROGRESS** * (arteck) add new eslint file * (arteck) fix node message +* (arteck) refactor ### 2.3.0 (2024-04-26) * (mcm1957) Adapter requires node.js >= 18 and js-controller >= 5 now diff --git a/lib/proxmox.js b/lib/proxmox.js index 91be555..2e8c0aa 100644 --- a/lib/proxmox.js +++ b/lib/proxmox.js @@ -45,60 +45,65 @@ class ProxmoxUtils { } async getNodes() { - return new Promise((resolve, reject) => { - if (this.responseCache['/nodes']) { - resolve(JSON.parse(JSON.stringify(this.responseCache['/nodes']))); + if (this.responseCache['/nodes']) { + return JSON.parse(JSON.stringify(this.responseCache['/nodes'])); + } + + try { + const data = await this._getData('/nodes', 'get'); + + if (data && typeof data === 'object') { + this.responseCache['/nodes'] = data.data; + return data.data; } else { - this._getData('/nodes', 'get') - .then((data) => { - if (data !== '' && data !== null && typeof data === 'object') { - this.responseCache['/nodes'] = data.data; - resolve(data.data); - } else { - reject(); - } - }) - .catch(reject); + throw new Error('Invalid data received'); } - }); + } catch (error) { + throw error; + } } + async getNodeStatus(node, useCache) { - return new Promise((resolve, reject) => { - if (useCache && this.responseCache[`/nodes/${node}/status`]) { - resolve(JSON.parse(JSON.stringify(this.responseCache[`/nodes/${node}/status`]))); + const cacheKey = `/nodes/${node}/status`; + + if (useCache && this.responseCache[cacheKey]) { + return JSON.parse(JSON.stringify(this.responseCache[cacheKey])); + } + + try { + const data = await this._getData(cacheKey, 'get'); + + if (data && typeof data === 'object') { + this.responseCache[cacheKey] = data.data; + return data.data; } else { - this._getData(`/nodes/${node}/status`, 'get') - .then((data) => { - if (data !== '' && data !== null && typeof data === 'object') { - this.responseCache[`/nodes/${node}/status`] = data.data; - resolve(data.data); - } else { - reject(); - } - }) - .catch(reject); + throw new Error('Invalid data received'); } - }); + } catch (error) { + throw error; + } } async getNodeDisks(node, useCache) { - return new Promise((resolve, reject) => { - if (useCache && this.responseCache[`/nodes/${node}/disks/list`]) { - resolve(JSON.parse(JSON.stringify(this.responseCache[`/nodes/${node}/disks/list`]))); + const cacheKey = `/nodes/${node}/disks/list`; + + if (useCache && this.responseCache[cacheKey]) { + return JSON.parse(JSON.stringify(this.responseCache[cacheKey])); + } + + try { + const data = await this._getData(cacheKey, 'get'); + + if (data && typeof data === 'object') { + this.responseCache[cacheKey] = data.data; + return data.data; } else { - this._getData(`/nodes/${node}/disks/list`, 'get') - .then((data) => { - if (data !== '' && data !== null && typeof data === 'object') { - this.responseCache[`/nodes/${node}/disks/list`] = data.data; - resolve(data.data); - } else { - reject(); - } - }) - .catch(reject); + throw new Error('Invalid data received'); } - }); + } catch (error) { + throw error; + } } async getNodeDisksSmart(node, disk) { @@ -114,83 +119,88 @@ class ProxmoxUtils { } async getClusterResources(useCache) { - return new Promise((resolve, reject) => { - if (useCache && this.responseCache['/cluster/resources']) { - resolve(JSON.parse(JSON.stringify(this.responseCache['/cluster/resources']))); + const cacheKey = '/cluster/resources'; + + if (useCache && this.responseCache[cacheKey]) { + return JSON.parse(JSON.stringify(this.responseCache[cacheKey])); + } + + try { + const data = await this._getData(cacheKey, 'get'); + + if (data && typeof data === 'object') { + this.responseCache[cacheKey] = data.data; + return data.data; } else { - return this._getData('/cluster/resources', 'get') - .then((data) => { - if (data !== '' && data !== null && typeof data === 'object') { - this.responseCache['/cluster/resources'] = data.data; - resolve(data.data); - } else { - reject(); - } - }) - .catch(reject); + throw new Error('Invalid data received'); } - }); + } catch (error) { + throw error; + } } async getResourceStatus(node, type, ID, useCache) { - return new Promise((resolve, reject) => { - if (useCache && this.responseCache[`/nodes/${node}/${type}/${ID}/status/current`]) { - resolve(JSON.parse(JSON.stringify(this.responseCache[`/nodes/${node}/${type}/${ID}/status/current`]))); + const cacheKey = `/nodes/${node}/${type}/${ID}/status/current`; + + if (useCache && this.responseCache[cacheKey]) { + return JSON.parse(JSON.stringify(this.responseCache[cacheKey])); + } + + try { + const data = await this._getData(cacheKey, 'get'); + + if (data && typeof data === 'object') { + this.responseCache[cacheKey] = data.data; + return data.data; } else { - this._getData(`/nodes/${node}/${type}/${ID}/status/current`, 'get') - .then((data) => { - if (data !== '' && data !== null && typeof data === 'object') { - this.responseCache[`/nodes/${node}/${type}/${ID}/status/current`] = data.data; - resolve(data.data); - } else { - reject(); - } - }) - .catch(reject); + throw new Error('Invalid data received'); } - }); + } catch (error) { + throw error; + } } async getBackupStatus(node, storage) { - return new Promise((resolve, reject) => { - this._getData(`/nodes/${node}/storage/${storage}/content`, 'get', '', '', 'storage') - .then((data) => { - if (data !== '' && data !== null && typeof data === 'object') { - resolve(data.data); - } else { - reject(); - } - }) - .catch(reject); - }); + try { + const data = await this._getData(`/nodes/${node}/storage/${storage}/content`, 'get', '', '', 'storage'); + + if (data && typeof data === 'object') { + return data.data; + } else { + throw new Error('Invalid data received'); + } + } catch (error) { + throw error; + } } async getStorageStatus(node, ID, shared, useCache) { - return new Promise((resolve, reject) => { - if (useCache && this.responseCache[`/nodes/${shared ? 'SHARED' : node}/storage/${ID}/status`]) { - resolve(JSON.parse(JSON.stringify(this.responseCache[`/nodes/${shared ? 'SHARED' : node}/storage/${ID}/status`]))); - } else if (shared) { - if (this.sharedMap[`/nodes/SHARED/storage/${ID}/status`]) { - node = this.sharedMap[`/nodes/SHARED/storage/${ID}/status`]; - } else { - this.sharedMap[`/nodes/SHARED/storage/${ID}/status`] = node; - } - } + const cacheKey = `/nodes/${shared ? 'SHARED' : node}/storage/${ID}/status`; + + if (useCache && this.responseCache[cacheKey]) { + return JSON.parse(JSON.stringify(this.responseCache[cacheKey])); + } - this._getData(`/nodes/${node}/storage/${ID}/status`, 'get', '','', 'storage') - .then((data) => { - if (data !== '' && data !== null && typeof data === 'object') { - this.responseCache[`/nodes/${shared ? 'SHARED' : node}/storage/${ID}/status`] = data.data; - resolve(data.data); - } else { - this.adapter.log.error(`Problem with getStorageStatus. ${JSON.stringify(data)}`); - } - reject(); - }) - .catch(reject); - }); + if (shared) { + node = this.sharedMap[cacheKey] || (this.sharedMap[cacheKey] = node); + } + + try { + const data = await this._getData(`/nodes/${node}/storage/${ID}/status`, 'get', '', '', 'storage'); + + if (data && typeof data === 'object') { + this.responseCache[cacheKey] = data.data; + return data.data; + } else { + this.adapter.log.error(`Problem with getStorageStatus. ${JSON.stringify(data)}`); + throw new Error('Invalid data received'); + } + } catch (error) { + throw error; + } } + async qemuStart(node, type, ID) { return this._getData(`/nodes/${node}/${type}/${ID}/status/start`, 'post'); } @@ -227,138 +237,108 @@ class ProxmoxUtils { return this._getData(`/nodes/${node}/status`, 'post', 'command=shutdown'); } - ticket(cb) { - this._getTicket() - .then((data) => { - this.adapter.log.debug(`Updating ticket to "${data.ticket}" and CSRF to "${data.CSRFPreventionToken}"`); - - TICKET = 'PVEAuthCookie=' + data.ticket; - CSRF = data.CSRFPreventionToken; - cb(); - }) - .catch((_error) => { - // this.adapter.log.error(`Unable to update ticket: ${error}`); - cb(); - }); - } + async ticket() { + //try { + const data = await this._getTicket(); - async _getData(url, method, data, retry, additional) { - return new Promise((resolve, reject) => { - if (this.stopped) { - reject('STOPPED'); - } + this.adapter.log.debug(`Updating ticket to "${data.ticket}" and CSRF to "${data.CSRFPreventionToken}"`); - if (typeof data === 'undefined') { - data = null; - } - if (typeof retry === 'undefined') { - retry = null; - } + TICKET = `PVEAuthCookie=${data.ticket}`; + CSRF = data.CSRFPreventionToken; + + /* } catch (error) { + this.adapter.log.error(`Unable to update ticket: ${error.message || error}`); + }*/ + } + + async _getData(url, method, data = null, retry = false, additional = null) { + if (this.stopped) { + throw 'STOPPED'; + } - const pathU = url || ''; + const pathU = url || ''; - axios({ + try { + const response = await axios({ method, baseURL: this.URL, url: pathU, data, - timeout: 10000, // only wait for 2s + timeout: 10000, headers: { CSRFPreventionToken: CSRF, Cookie: TICKET, }, - validateStatus: (status) => { - return [200, 401, 500, 595, 599].indexOf(status) > -1; - }, - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }) - .then((response) => { - this.adapter.log.debug(`received ${response.status} response from ${pathU} with content: ${JSON.stringify(response.data)}`); - - if (response.status === 500 || response.status === 595 || response.status === 599) { - reject(response.status + ' - ' + response.data); - } - - if (response.status === 401 && !retry) { - if (additional == null) { - this.ticket(async () => { - // Retry with new ticket - this._getData(url, method, data, true).then(resolve).catch(reject); - }); - } - } - if (response.status === 200) { - resolve(response.data); - } - }) - .catch((_error) => { - if (additional != 'storage') { - - this.adapter.log.warn(`${additional} -- Use Next Proxmox Host because of communication failure ${this.URL}${url}`); - - // select next server and try again - this.setNextUrlMain(); - - this.ticket(async () => { - // Retry with new ticket - this._getData(url, method, data, true).then(resolve).catch(reject); - }); - } - if (additional == 'storage') { - this.adapter.log.error(`Check ${additional} -- Problem found.. maybe offline check path ${this.URL}${url}'`); - } - }); - }); + validateStatus: (status) => [200, 401, 500, 595, 599].includes(status), + httpsAgent: new https.Agent({ rejectUnauthorized: false }), + }); + + this.adapter.log.debug(`received ${response.status} response from ${pathU} with content: ${JSON.stringify(response.data)}`); + + if ([500, 595, 599].includes(response.status)) { + throw `${response.status} - ${response.data}`; + } + + if (response.status === 401 && !retry) { + if (!additional) { + await this.ticket(); + return this._getData(url, method, data, true); + } + } + + if (response.status === 200) { + return response.data; + } + } catch (error) { + if (additional !== 'storage') { + this.adapter.log.warn(`${additional} -- Use Next Proxmox Host because of communication failure ${this.URL}${url}`); + + this.setNextUrlMain(); + await this.ticket(); + return this._getData(url, method, data, true); + } + + if (additional === 'storage') { + this.adapter.log.error(`Check ${additional} -- Problem found.. maybe offline check path ${this.URL}${url}`); + } + + throw error; + } } async _getTicket() { - return new Promise((resolve, reject) => { - const url = `/access/ticket?username=${encodeURIComponent(this.name)}@${encodeURIComponent(this.server)}&password=${encodeURIComponent(this.password)}`; + const url = `/access/ticket?username=${encodeURIComponent(this.name)}@${encodeURIComponent(this.server)}&password=${encodeURIComponent(this.password)}`; - axios({ + try { + const response = await axios({ method: 'post', baseURL: this.URL, url, - timeout: 5000, // only wait for 2s - httpsAgent: new https.Agent({ - rejectUnauthorized: false, - }), - }) - .then((response) => { - this.adapter.log.debug(`received ${response.status} response from proxmox with content: ${JSON.stringify(response.data)}`); - - if (response.status === 200) { - this.adapter.log.debug(`dataticket: ${JSON.stringify(response.data)}`); - - resolve(response.data.data); // "data" is an attribute in the response json - } else { - this.adapter.log.error(`${response.status}: wrong User data, could not log in, please try again with correct user and pw`); - - reject(response.status); - } - }) - .catch((error) => { - if (error.response) { - // The request was made and the server responded with a status code - this.adapter.log.warn(`Error received ${error.response.status} response from proxmox with content: ${JSON.stringify(error.response.data)}`); - - reject(error.response.status); - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - - reject(-1); - } else { - // Something happened in setting up the request that triggered an Error - this.adapter.log.error(error.message); - - reject(-2); - } - }); - }); + timeout: 5000, + httpsAgent: new https.Agent({ rejectUnauthorized: false }), + }); + + this.adapter.log.debug(`received ${response.status} response from proxmox with content: ${JSON.stringify(response.data)}`); + + if (response.status === 200) { + this.adapter.log.debug(`dataticket: ${JSON.stringify(response.data)}`); + return response.data.data; // "data" is an attribute in the response JSON + } else { + this.adapter.log.error(`${response.status}: wrong User data, could not log in, please try again with correct user and pw`); + throw new Error(response.status); + } + } catch (error) { + if (error.response) { + this.adapter.log.warn(`Error received ${error.response.status} response from proxmox with content: ${JSON.stringify(error.response.data)}`); + throw new Error(error.response.status); + } else if (error.request) { + this.adapter.log.warn(`No response received from proxmox`); + throw new Error(-1); + } else { + this.adapter.log.error(`Request setup error: ${error.message}`); + throw new Error(-2); + } + } } stop() { diff --git a/main.js b/main.js index 6c95f63..83d9c4c 100644 --- a/main.js +++ b/main.js @@ -65,17 +65,17 @@ class Proxmox extends utils.Adapter { try { // Get a new ticket (login) - this.proxmox.ticket(async () => { - await this.readObjects(); - await this.getNodes(); + await this.proxmox.ticket(); + await this.readObjects(); + await this.getNodes(); - // subscribe on all state changes - await this.subscribeStatesAsync('*'); + // subscribe on all state changes + await this.subscribeStatesAsync('*'); - this.sendRequest(); // start interval + this.sendRequest(); // start interval + + await this.setStateAsync('info.connection', { val: true, ack: true }); - await this.setStateAsync('info.connection', { val: true, ack: true }); - }); } catch (err) { await this.setStateAsync('info.connection', { val: false, ack: true }); @@ -958,10 +958,6 @@ class Proxmox extends utils.Adapter { await this.setStateChangedAsync(`${sid}.status`, { val: node.status, ack: true }); if (node.status !== 'offline') { - if (this.config.requestCephInformation && node.type === 'node') { - await this.setCeph(); - } - await this.setStateChangedAsync(`${sid}.cpu`, { val: parseInt(node.cpu * 10000) / 100, ack: true }); if (node.maxcpu) { await this.setStateChangedAsync(`${sid}.cpu_max`, { val: node.maxcpu, ack: true }); @@ -1111,6 +1107,10 @@ class Proxmox extends utils.Adapter { } } + if (this.config.requestCephInformation) { + await this.setCeph(); + } + if (this.config.requestHAInformation) { await this.setHA(); } @@ -1118,11 +1118,12 @@ class Proxmox extends utils.Adapter { await this.setVM(); } async setCeph() { + const cephid = `${this.namespace}.ceph`; try { - const cephid = `${this.namespace}.ceph`; - const cephInformation = await this.proxmox.getCephInformation(); + this.log.debug('cephInformation: ' + JSON.stringify(cephInformation)); + if (cephInformation !== null) { for (const lpEntry in cephInformation.data) { const lpType = typeof cephInformation.data[lpEntry]; // get Type of Variable as String, like string/number/boolean @@ -1142,15 +1143,17 @@ class Proxmox extends utils.Adapter { } } } catch (err) { - this.log.error(`Unable to get Ceph resources: ${err.message} `); + this.log.error('Unable to get Ceph resources: ' + JSON.stringify(err)); } } async setHA() { + const haid = `${this.namespace}.ha`; try { - const haid = `${this.namespace}.ha`; const haInformation = await this.proxmox.getHAStatusInformation(); + this.log.debug('haInformation: ' + JSON.stringify(haInformation)); + for (const lpEntry in haInformation.data) { const lpType = typeof haInformation.data[lpEntry]; // get Type of Variable as String, like string/number/boolean const lpData = haInformation.data[lpEntry]; diff --git a/package.json b/package.json index e39f144..94e55d3 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@types/sinon": "^17.0.3", "@types/sinon-chai": "^3.2.12", "chai": "^4.5.0", - "chai-as-promised": "^8.0.0", + "chai-as-promised": "^8.0.1", "eslint": "^9.17.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1",