@ -2,21 +2,11 @@
const Promise = require ( "bluebird" ) ;
const graphql = require ( "graphql" ) ;
const DataLoader = require ( "dataloader" ) ;
const util = require ( "util" ) ;
const fs = require ( "fs" ) ;
const path = require ( "path" ) ;
const chalk = require ( "chalk" ) ;
const matchOrError = require ( "./match-or-error" ) ;
const lsblk = require ( "./wrappers/lsblk" ) ;
const smartctl = require ( "./wrappers/smartctl" ) ;
const lvm = require ( "./wrappers/lvm" ) ;
const upperSnakeCase = require ( "./upper-snake-case" ) ;
function gql ( strings ) {
return strings . join ( "" ) ;
}
const gql = require ( "./graphql/tag" ) ;
const api = require ( "./api/index" ) ;
function debugDisplay ( results ) {
if ( results . errors != null && results . errors . length > 0 ) {
@ -52,370 +42,9 @@ function debugDisplay(results) {
console . log ( util . inspect ( results . data , { colors : true , depth : null } ) ) ;
}
/* FIXME: This seems to be added into a global registry somehow? How to specify this explicitly on a query without relying on globals? */
new graphql . GraphQLScalarType ( {
name : "ByteSize" ,
description : "A value that represents a value on a byte scale" ,
serialize : ( value ) => {
return JSON . stringify ( value ) ;
} ,
parseValue : ( value ) => {
return JSON . parse ( value ) ;
} ,
parseLiteral : ( value ) => {
return JSON . parse ( value ) ;
} ,
} ) ;
new graphql . GraphQLScalarType ( {
name : "TimeSize" ,
description : "A value that represents a value on a time scale" ,
serialize : ( value ) => {
return JSON . stringify ( value ) ;
} ,
parseValue : ( value ) => {
return JSON . parse ( value ) ;
} ,
parseLiteral : ( value ) => {
return JSON . parse ( value ) ;
} ,
} ) ;
function withProperty ( dataSource , id , property ) {
return withData ( dataSource , id , ( value ) => {
return value [ property ] ;
} ) ;
}
function withData ( dataSource , id , callback ) {
return function ( _ , { data } ) {
return Promise . try ( ( ) => {
if ( data [ dataSource ] != null ) {
return data [ dataSource ] . load ( id ) ;
} else {
throw new Error ( ` Specified data source ' ${ dataSource } ' does not exist ` ) ;
}
} ) . then ( ( value ) => {
if ( value != null ) {
return callback ( value ) ;
} else {
throw new Error ( ` Got a null value from data source ' ${ dataSource } ' for ID ' ${ id } ' ` ) ;
}
} ) ;
} ;
}
let All = Symbol ( "All" ) ;
function createLoaders ( ) {
/* The below is to ensure that commands that produce a full list of all possible items, only ever get called and processed *once* per query, no matter what data is requested. */
let lsblkPromise ;
let smartctlPromise ;
let lvmPhysicalVolumesPromise ;
return {
lsblk : new DataLoader ( ( names ) => {
return Promise . try ( ( ) => {
if ( lsblkPromise == null ) {
lsblkPromise = Promise . try ( ( ) => {
return lsblk ( ) ;
} ) . then ( ( devices ) => {
return {
tree : devices ,
list : linearizeDevices ( devices )
} ;
} ) ;
}
return lsblkPromise ;
} ) . then ( ( { tree , list } ) => {
return names . map ( ( name ) => {
if ( name === All ) {
return tree ;
} else {
return list . find ( ( device ) => device . name === name ) ;
}
} ) ;
} ) ;
} ) ,
smartctlScan : new DataLoader ( ( paths ) => {
return Promise . try ( ( ) => {
if ( smartctlPromise == null ) {
smartctlPromise = smartctl . scan ( ) ;
}
return smartctlPromise ;
} ) . then ( ( devices ) => {
return paths . map ( ( path ) => {
if ( path === All ) {
return devices ;
} else {
return devices . find ( ( device ) => device . path === path ) ;
}
} ) ;
} ) ;
} ) ,
smartctlInfo : new DataLoader ( ( paths ) => {
return Promise . map ( paths , ( path ) => {
return smartctl . info ( { devicePath : path } ) ;
} ) ;
} ) ,
smartctlAttributes : new DataLoader ( ( paths ) => {
return Promise . map ( paths , ( path ) => {
return smartctl . attributes ( { devicePath : path } ) ;
} ) ;
} ) ,
lvmPhysicalVolumes : new DataLoader ( ( paths ) => {
return Promise . try ( ( ) => {
if ( lvmPhysicalVolumesPromise == null ) {
lvmPhysicalVolumesPromise = lvm . getPhysicalVolumes ( ) ;
}
return lvmPhysicalVolumesPromise ;
} ) . then ( ( volumes ) => {
return paths . map ( ( path ) => {
if ( path === All ) {
return volumes ;
} else {
return volumes . find ( ( device ) => device . path === path ) ;
}
} ) ;
} ) ;
} ) ,
} ;
}
let ID = Symbol ( "ID" ) ;
let LocalProperties = Symbol ( "localProperties" ) ;
function createDataObject ( mappings ) {
let object = { } ;
if ( mappings [ LocalProperties ] != null ) {
Object . assign ( object , mappings [ LocalProperties ] ) ;
}
for ( let [ dataSource , items ] of Object . entries ( mappings ) ) {
if ( items [ ID ] != null ) {
let id = items [ ID ] ;
for ( let [ property , source ] of Object . entries ( items ) ) {
if ( typeof source === "string" ) {
object [ property ] = withProperty ( dataSource , id , source ) ;
} else if ( typeof source === "function" ) {
object [ property ] = withData ( dataSource , id , source ) ;
}
}
} else {
throw new Error ( ` No object ID was provided for the ' ${ dataSource } ' data source ` ) ;
}
}
return object ;
}
// ###############################################
let schema = graphql . buildSchema ( fs . readFileSync ( path . resolve ( _ _dirname , "./schemas/main.gql" ) , "utf8" ) ) ;
function createBlockDevice ( { name , path } ) {
if ( name != null ) {
path = ` /dev/ ${ name } ` ;
} else if ( path != null ) {
let match = matchOrError ( /^\/dev\/(.+)$/ , path ) ;
name = match [ 0 ] ;
}
/* FIXME: parent */
return createDataObject ( {
[ LocalProperties ] : {
path : path
} ,
lsblk : {
[ ID ] : name ,
name : "name" ,
size : "size" ,
mountpoint : "mountpoint" ,
deviceNumber : "deviceNumber" ,
removable : "removable" ,
readOnly : "readOnly" ,
children : ( device ) => {
return device . children . map ( ( child ) => {
return createBlockDevice ( { name : child . name } ) ;
} ) ;
}
}
} ) ;
}
function createPhysicalVolume ( { path } ) {
return createDataObject ( {
[ LocalProperties ] : {
path : path ,
blockDevice : ( ) => {
return createBlockDevice ( { path : path } ) ;
}
} ,
lvmPhysicalVolumes : {
[ ID ] : path ,
volumeGroup : ( volume ) => {
if ( volume . volumeGroup != null ) {
return createVolumeGroup ( { name : volume . volumeGroup } ) ;
}
} ,
format : "format" ,
size : "totalSpace" ,
freeSpace : "freeSpace" ,
duplicate : "isDuplicate" ,
allocatable : "isAllocatable" ,
used : "isUsed" ,
exported : "isExported" ,
missing : "isMissing"
}
} ) ;
}
function createVolumeGroup ( { name } ) {
return createDataObject ( {
[ LocalProperties ] : {
name : name
}
} ) ;
}
function createDrive ( { path } ) {
return createDataObject ( {
[ LocalProperties ] : {
path : path ,
blockDevice : ( ) => {
return createBlockDevice ( { path : path } ) ;
} ,
/* FIXME: allBlockDevices, for representing every single block device that's hosted on this physical drive, linearly. Need to figure out how that works with representation of mdraid arrays, LVM volumes, etc. */
} ,
smartctlScan : {
[ ID ] : path ,
interface : "interface"
} ,
smartctlInfo : {
[ ID ] : path ,
model : "model" ,
modelFamily : "modelFamily" ,
smartAvailable : "smartAvailable" ,
smartEnabled : "smartEnabled" ,
serialNumber : "serialNumber" ,
wwn : "wwn" ,
firmwareVersion : "firmwareVersion" ,
size : "size" ,
rpm : "rpm" ,
logicalSectorSize : ( device ) => device . sectorSizes . logical ,
physicalSectorSize : ( device ) => device . sectorSizes . physical ,
formFactor : "formFactor" ,
ataVersion : "ataVersion" ,
sataVersion : "sataVersion"
} ,
smartctlAttributes : {
[ ID ] : path ,
smartAttributes : ( attributes ) => {
return attributes . map ( ( attribute ) => {
return Object . assign ( { } , attribute , {
type : upperSnakeCase ( attribute . type ) ,
updatedWhen : upperSnakeCase ( attribute . updatedWhen )
} ) ;
} ) ;
} ,
smartHealth : ( attributes ) => {
let failed = attributes . filter ( ( item ) => {
return ( item . failingNow === true || item . failedBefore === true ) ;
} ) ;
let deteriorating = attributes . filter ( ( item ) => {
return ( item . type === "preFail" && item . worstValueSeen < 100 ) ;
} ) ;
if ( failed . length > 0 ) {
return "FAILING" ;
} else if ( deteriorating . length > 0 ) {
return "DETERIORATING" ;
} else {
return "HEALTHY" ;
}
}
}
} ) ;
}
function linearizeDevices ( devices ) {
let linearizedDevices = [ ] ;
function add ( list ) {
for ( let device of list ) {
linearizedDevices . push ( device ) ;
if ( device . children != null ) {
add ( device . children ) ;
}
}
}
add ( devices ) ;
return linearizedDevices ;
}
let root = {
hardware : {
drives : function ( { paths } , { data } ) {
return Promise . try ( ( ) => {
if ( paths != null ) {
return data . smartctlScan . loadMany ( paths ) ;
} else {
return data . smartctlScan . load ( All ) ;
}
} ) . then ( ( devices ) => {
return devices . map ( ( device ) => {
return createDrive ( { path : device . path } ) ;
} ) ;
} ) ;
}
} ,
resources : {
blockDevices : function ( { names } , { data } ) {
return Promise . try ( ( ) => {
if ( names != null ) {
return data . lsblk . loadMany ( names ) ;
} else {
return data . lsblk . load ( All ) ;
}
} ) . then ( ( devices ) => {
return devices . map ( ( device ) => {
return createBlockDevice ( { name : device . name } ) ;
} ) ;
} ) ;
} ,
lvm : {
physicalVolumes : function ( { paths } , { data } ) {
return Promise . try ( ( ) => {
if ( paths != null ) {
return data . lvmPhysicalVolumes . loadMany ( paths ) ;
} else {
return data . lvmPhysicalVolumes . load ( All ) ;
}
} ) . then ( ( volumes ) => {
return volumes . map ( ( volume ) => {
return createPhysicalVolume ( { path : volume . path } ) ;
} ) ;
} ) ;
}
}
}
} ;
function makeQuery ( query , args ) {
return graphql . graphql ( schema , query , root , {
data : createLoaders ( )
} , args ) ;
}
let makeQuery = api ( ) ;
// FIXME: If we intend to target macOS, a lot of whitespace-based output splitting won't work: https://www.mail-archive.com/austin-group-l@opengroup.org/msg01678.html