UNPKG

node-snapd

Version:

nodejs client interface to ubuntu core snapd api

479 lines (407 loc) 11.2 kB
'use strict' const http = require('http') const path = require('path') const fs = require('fs') const os = require('os') const reach = require('./reach') /** * Represents a snapd formatter error * @class * @param {object} body */ class SnapdError extends Error { constructor(body){ super(reach(body, 'result.message') || 'something went wrong') Error.captureStackTrace(this, SnapdError) this.code = reach(body, 'result.kind') } } /** * A snap client instance which makes restful call to snapd process using Unix sockets * @class * @param {string} authFile Defaults to `$HOME/.snap/auth.json` * @param {string} socketPath Defaults to `/run/snapd.socket` */ class SnapClient { constructor( authFile=path.join(os.homedir(), '.snap', 'auth.json'), socketPath=`/run/snapd.socket` ){ this.auth = undefined this.socketPath = socketPath } /** * make restful call to snapd process thru /run/snapd.socket * @method * @param {object} options * @param {object} options.auth * @param {string} options.method * @param {string} options.path * @param {string} options.data */ async rest({auth, method, path, data}) { return new Promise((resolve, reject) => { const post = method === 'POST' const headers = { 'Content-Type': 'application/json' } if (post) { headers['Content-Length'] = Buffer.byteLength(data) } if (auth && typeof auth.macaroon === 'string') { headers['Authorization'] = `Macaroon root="${auth.macaroon}"` } const options = { socketPath: this.socketPath, path: path, method: post ? 'POST' : 'GET', headers: headers } const req = http.request(options, (res) => { res.setEncoding('utf8') let body = '' res.on('data', (chunk) => { body += chunk }) res.on('end', () => { if (res.statusCode === 200 || res.statusCode === 202) { if (body.length < 1) { return reject(new Error('empty response')) } const json = JSON.parse(body) return resolve(json) } const json = JSON.parse(body) return reject(new SnapdError(json)) }) }) req.on('error', (error) => { return reject(error) }) // write data to request body if (post && typeof data === 'string') { req.write(data) } req.end() }) } /** * read auth email & macaroon from ~/.snap/auth.json * @method * @param {string} filename */ async readAuth(filename) { if(this.auth){ return Promise.resolve({ email: this.auth.email, macaroon: this.auth.macaroon }) } return new Promise((resolve, reject) => { const fn = filename || path.join(os.homedir(), '.snap', 'auth.json') fs.readFile(fn, (error, data) => { if (error) { return reject(error) } this.auth = JSON.parse(data) if (typeof this.auth.macaroon !== 'string') { return reject(new Error('failed to read macaroon from auth file')) } return resolve({ email: this.auth.email, macaroon: this.auth.macaroon }) }) }) } /** * login may require root privilege * @method * @param {object} options * @param {object} options.auth * @param {string} options.method * @param {string} options.path * @param {string} options.data */ async login({ email, password, otp }) { const data = { email: email, password: password, otp: otp // one time passkey for 2fa } const response = await this.rest({ method: 'POST', path: '/v2/login', data: JSON.stringify(data) }) if (response && response['status-code'] === 200) { return { email: response.result.email, macaroon: response.result.macaroon } } return Promise.reject(new Error('malformed response')) } /** * logout may require root privilege * @method * @param {object} options * @param {object} options.auth */ async logout({ auth }) { const response = await this.rest({ auth: auth || await this.readAuth(), method: 'POST', path: '/v2/logout' }) if (response && response['status-code'] === 200) { return true } return Promise.reject(new Error('malformed response')) } /** * list of installed snaps * @method */ async listSnaps() { const response = await this.rest({ method: 'GET', path: '/v2/snaps' }) if (response && response['status-code'] === 200) { return response.result.map(entry => entry.name) } throw new Error('malformed response') } /** * Get detailed info about a snap * @method * @param {object} options * @param {object} options.name */ async info({ name }) { if (typeof name !== 'string') { return Promise.reject(new Error('malformed name argument')) } const response = await this.rest({ method: 'GET', path: `/v2/snaps/${name}` }) if (response && response['status-code'] === 200) { return response.result } return Promise.reject(new Error('malformed response')) } /** * * @method * @param {object} options * @param {string} options.action * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async modify({ action, name, auth, ...opts }) { if (typeof name !== 'string') { return Promise.reject(new Error('malformed name argument')) } const data = { action: action } if (opts) { for (const b of ['classic', 'devmode', 'ignore-validation', 'jailmode']) { if (opts[b]) { data[b] = true } } for (const str of ['channel', 'version']) { if (typeof opts[str] === 'string') { data[str] = opts[str] } } } const response = await this.rest({ auth: auth || await this.readAuth(), method: 'POST', path: `/v2/snaps/${name}`, data: JSON.stringify(data) }) if (response && response['status-code'] === 202) { return response.change } return Promise.reject(new Error('malformed response')) } /** * * @method * @param {object} options * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async install ({ name, auth, ...opts }) { return this.modify({ action: 'install', name, auth, ...opts }) } /** * * @method * @param {object} options * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async remove ({ name, auth, ...opts }) { return this.modify({ action: 'remove', name, auth, ...opts }) } /** * * @method * @param {object} options * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async switch ({ name, auth, ...opts }) { return this.modify({ action: 'switch', name, auth, ...opts }) } /** * * @method * @param {object} options * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async refresh ({ name, auth, ...opts }) { return this.modify({ action: 'refresh', name, auth, ...opts }) } /** * * @method * @param {object} options * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async revert ({ name, auth, ...opts }) { return this.modify({ action: 'revert', name, auth, ...opts }) } /** * * @method * @param {object} options * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async enable ({ name, auth, ...opts }) { return this.modify({ action: 'enable', name, auth, ...opts }) } /** * * @method * @param {object} options * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async disable ({ name, auth, ...opts }) { return this.modify({ action: 'disable', name, auth, ...opts }) } /** * check on status of change by id (or all changes without id arg) * * @method * @param {object} options * @param {string} options.id */ async status ({ id }) { const response = await this.rest({ method: 'GET', path: id ? `/v2/changes/${id}` : '/v2/changes' }) if (response && response['status-code'] === 200) { return response.result } return Promise.reject(new Error('malformed response')) } /** * abort ongoing change by id * * @method * @param {object} options * @param {string} options.id * @param {object} options.auth */ async abort ({ id, auth }) { if (typeof id !== 'string') { return Promise.reject(new Error('malformed id argument')) } const response = await this.rest({ auth: auth || await this.readAuth(), method: 'POST', path: `/v2/changes/${id}`, data: JSON.stringify({ action: 'abort' }) }) if (response && response['status-code'] === 200) { return response.result } return Promise.reject(new Error('malformed response')) } /** * * @method * @param {object} options * @param {object} options.auth */ async listInterfaces ({ auth }={}) { const response = await this.rest({ auth: auth || await this.readAuth(), method: 'GET', path: '/v2/interfaces' }) if (response && response['status-code'] === 200) { return response.result } return Promise.reject(new Error('malformed response')) } /** * * @method * @param {object} options * @param {string} options.action * @param {object} options.slot * @param {object} options.plug * @param {object} options.auth */ async modifyInterface ({ action, slot, plug, auth }) { const data = { action: action, slots: [{ snap: slot.snap, slot: slot.slot }], plugs: [{ snap: plug.snap, plug: plug.plug }] } const response = await this.rest({ auth: auth || await this.readAuth(), method: 'POST', path: '/v2/interfaces', data: JSON.stringify(data) }) if (response && response['status-code'] === 202) { return response.change } return Promise.reject(new Error('malformed response')) } /** * * @method * @param {object} options * @param {object} options.slot * @param {object} options.plug * @param {object} options.auth */ async connect ({ slot, plug, auth }) { return this.modifyInterface({ action: 'connect', slot, plug, auth }) } /** * * @method * @param {object} options * @param {string} options.name * @param {object} options.auth * @param {object} options.opts */ async disconnect ({ slot, plug, auth }) { return this.modifyInterface({ action: 'disconnect', slot, plug, auth }) } } module.exports = SnapClient