<commit_before>/*
 * Copyright (c) 2012, Joyent, Inc. All rights reserved.
 *
 * This file contains all the Compute Server logic, used to interface with the
 * server as well as it's stored representation in the backend datastores.
 */

var async = require('async');
var sprintf = require('sprintf').sprintf;
var util = require('util');
var common = require('../common');
var buckets = require('../apis/moray').BUCKETS;
var verror = require('verror');

var ModelBase = require('./base');
var ModelVM = require('./vm');

var PROVISIONER = 'provisioner';

/**
 * RFC 2254 Escaping of filter strings
 * @author [<NAME>](https://github.com/ozten)
 */

function filterEscape(inp) {
    if (typeof (inp) === 'string') {
        var esc = '';
        for (var i = 0; i < inp.length; i++) {
            switch (inp[i]) {
                case '*':
                    esc += '\\2a';
                    break;
                case '(':
                    esc += '\\28';
                    break;
                case ')':
                    esc += '\\29';
                    break;
                case '\\':
                    esc += '\\5c';
                    break;
                case '\0':
                    esc += '\\00';
                    break;
                default:
                    esc += inp[i];
                    break;
            }
        }

        return esc;
    } else {
        return inp;
    }
}


/**
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 * ModelServer encapsulates the logic for manipulating and interacting
 * servers and their back-end storage representation.
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 */

function ModelServer(uuid) {
    if (!uuid) {
        throw new Error('ModelServer missing uuid parameter');
    }

    this.value = null;
    this.uuid = uuid;

    this.log = ModelServer.getLog();
}


ModelServer.init = function (model) {
    this.model = model;

    Object.keys(ModelBase.staticFn).forEach(function (p) {
        ModelServer[p] = ModelBase.staticFn[p];
    });

    ModelServer.tasks = {};
    ModelServer.log = model.getLog();
};


ModelServer.createProvisionerEventHandler = function (model, jobuuid) {
    var self = this;
    var workflow = ModelServer.getWorkflow();

    return function (taskid, event) {
        if (!jobuuid) {
            return;
        }

        self.log.info(
            'Posting task info (task %s) to workflow jobs endpoint (job %s)',
            taskid, jobuuid);
        workflow.getClient().client.post(
            '/jobs/' + jobuuid + '/info',
            event,
            function (error, req, res, obj) {
                if (error) {
                    self.log.error(
                        error, 'Error posting info to jobs endpoint');
                    return;
                }
                self.log.info(
                    'Posted task info (task %s, job %s)',
                    taskid, jobuuid);
            });
    };
};


/**
 * Return a list of servers matching given criteria.
 */

ModelServer.list = function (params, callback) {
    var self = this;

    var uuid = params.uuid;

    var extras = params.extras || {};

    this.log.debug(params, 'Listing servers');

    var wantFinal = params.wantFinal;

    var filterParams = ['datacenter', 'setup', 'headnode'];
    var filter = '';

    if (Array.isArray(uuid)) {
        var uuidFilter = uuid.map(function (u) {
            return '(uuid=' + u + ')';
        }).join('');
        filter += '(|' + uuidFilter + ')';
    } else if (uuid) {
        filter += '(uuid=' + uuid + ')';
    } else {
        filter += '(uuid=*)';
    }

    var paramsFilter = [];
    filterParams.forEach(function (p) {
        if (!params.hasOwnProperty(p) || typeof (params[p]) === 'undefined') {
            return;
        }

        if (!buckets.servers.bucket.index[p]) {
            return;
        }

        if (buckets.servers.bucket.index[p].type === 'string') {
            paramsFilter.push(sprintf('(%s=%s)', p, filterEscape(params[p])));
        } else if (buckets.servers.bucket.index[p].type === 'number') {
            paramsFilter.push(sprintf('(%s=%s)', p, filterEscape(params[p])));
        } else if (buckets.servers.bucket.index[p].type === 'boolean') {
            paramsFilter.push(sprintf('(%s=%s)', p, filterEscape(params[p])));
        }
    });

    paramsFilter.push('!(uuid=default)');

    if (paramsFilter.length > 1) {
        filter = sprintf('(&%s(&%s))', filter, paramsFilter.join(''));
    } else if (paramsFilter.length === 1) {
        filter = sprintf('(&%s%s)', filter, paramsFilter[0]);
    }

    var moray = ModelServer.getMoray();

    var findOpts = {
            sort: {
                attribute: 'uuid',
                order: 'ASC'
            }
        };

    ['limit', 'offset'].forEach(function (f) {
        if (params.hasOwnProperty(f)) {
            findOpts[f] = params[f];
        }
    });

    var req = moray.findObjects(
        buckets.servers.name,
        filter,
        findOpts);

    var servers = [];

    req.on('error', onError);
    req.on('record', onRecord);
    req.on('end', processResults);

    function onError(error) {
        self.log.error(error, 'Error retriving results');
        callback(error);
    }

    function onRecord(server) {
        servers.push(server.value);
    }

    function processResults() {
        if (!wantFinal) {
            callback(null, servers);
            return;
        }

        async.map(
            servers,
            function (server, cb) {
                var serverModel = new ModelServer(server.uuid);
                serverModel.setRaw(server);

                serverModel.getFinal(extras, function (error, s) {
                    cb(null, s);
                });
            },
            function (error, results) {
                callback(null, results);
            });
    }
};


/**
 * Initiate a workflow, which can may be can be added to which is run whenever
 * a new server starts up and sends its sysinfo payload via Ur.
 */

ModelServer.beginSysinfoWorkflow = function (sysinfo, callback) {
    var self = this;

    var uuid = sysinfo.UUID;

    var params = {
        sysinfo: sysinfo,
        server_uuid: uuid,
        target: uuid,
        admin_uuid: ModelServer.getConfig().adminUuid
    };

    self.log.info('Instantiating server-sysinfo workflow');
    ModelServer.getWorkflow().getClient().createJob(
        'server-sysinfo',
        params,
        function (error, job) {
            if (error) {
                self.log.error('Error in workflow: %s', error.message);
                if (callback) {
                    callback(error);
                }
                return;
            }
            if (callback) {
                callback();
            }
        });
};

/**
 * Creates an object that will contain default values for new servers.
 */
ModelServer.setDefaultServer = function (values, callback) {
    var self = this;

    var defaultServer = {
        uuid: 'default',
        boot_platform: 'latest',
        default_console: 'vga',
        serial: 'ttyb',
        serial_speed: 115200,
        boot_params: {}
    };

    if (arguments.length === 2) {
        defaultServer = {
            uuid: 'default'
        };
        for (var k in values) {
            defaultServer[k] = values[k];
        }
    } else {
        defaultServer = {
            uuid: 'default',
            boot_platform: 'latest',
            default_console: 'vga',
            serial: 'ttyb',
            serial_speed: 115200,
            boot_params: {}
        };
        callback = arguments[0];
    }

    var moray = ModelServer.getMoray();

    moray.putObject(
        buckets.servers.name,
        'default',
        defaultServer,
        function (putError) {
            if (putError) {
                self.log.error('Could not store default server');
                callback(
                    verror.VError(putError, 'failed to store default server'));
                return;
            }
            callback();
        });
};

ModelServer.updateDefaultServer = function (values, callback) {
    var self = this;
    var moray = ModelServer.getMoray();

    moray.getObject(
        buckets.servers.name,
        'default',
        function (error, obj) {
            if (error) {
                self.log.error(error, 'Could not store default server');
                callback(verror.VError(
                    error, 'failed to store default server'));
                return;
            }

            var server = obj.value;
            if (!server.boot_params) {
                server.boot_params = {};
            }
            var boot_params = values.boot_params;

            for (var k in boot_params) {
                if (boot_params[k] === null) {
                    delete server.boot_params[k];
                    continue;
                }
                server.boot_params[k] = boot_params[k];
            }

            var names = ['default_console', 'serial', 'serial_speed'];

            names.forEach(function (n) {
                if (values[n] === null) {
                    delete server[n];
                    return;
                }
                server[n] = values[n] || server[n];
            });

            if (values.boot_platform) {
                server.boot_platform = values.boot_platform;
            }

            moray.putObject(
                buckets.servers.name,
                'default',
                server,
                function (putError) {
                    if (putError) {
                        self.log.error('Could not store default server');
                        callback(verror.VError(
                            putError, 'failed to store default server'));
                        return;
                    }
                    callback();
                });
        });
};

/**
 * Execute a command on a particular server via Ur.
 */

ModelServer.prototype.invokeUrScript =
function (script, params, callback) {
    var self = this;
    var uuid = this.uuid;

    var opts = {
        uuid: uuid,
        message: {
            type: 'script',
            script: script,
            args: params.args || [],
            env: params.env || {}
        }
    };
    self.log.info('Sending compute node %s script', uuid);

    ModelServer.getUr().execute(opts, function (err, stdout, stderr) {
        if (err) {
            self.log.error('Error raised by ur when ' +
                'running script: ' + err.message);
        }

        callback(err, stdout, stderr);
        return;
    });
};

/**
 * Reboot a server.
 */

ModelServer.prototype.reboot =
function (callback) {
    var self = this;

    self.invokeUrScript(
        '#!/bin/bash\nexit 113', {}, function (error) {
            callback(error);
        });
};


/**
 * Format an error message with
 */

ModelServer.prototype.errorFmt = function (str) {
    return sprintf('Error (server=%s): %s', this.uuid, str);
};


/**
 * Logs an error message along with the relevant error object.
 */

ModelServer.prototype.logerror = function (error, str) {
    var self = this;
    this.log.error(error, self.errorFmt(str));
};


/**
 * Fetches and returns the server's cached values and merges them with the raw
 * internal values.
 */

ModelServer.prototype.applyCachedValues = function (fields, callback) {
    var self = this;

    var server = clone(self.value);

    function isBoolean(a) {
        return !!a === a;
    }

    function skip(f, fo) {
        var ret = (
            !isBoolean(fo[f]) ||
            !fo[f]);
        return ret;
    }

    async.parallel([
        function (cb) {
            if (skip('status', fields)) {
                cb();
                return;
            }
            self.cacheGetServerStatus(
                function (statusError, status) {
                    if (statusError) {
                        self.logerror(
                            statusError,
                            'fetching server status');

                        cb(statusError);
                        return;
                    }

                    server.status = status || 'unknown';
                    self.log.debug(
                        'Status for %s was %s', self.uuid, server.status);
                    cb();
                });
        },
        function (cb) {
            if (skip('memory', fields)) {
                cb();
                return;
            }
            self.cacheGetMemoryUsage(
                function (cacheError, memory) {
                    if (cacheError) {
                        self.logerror(
                            cacheError,
                            'fetching memory values');

                        cb(cacheError);
                        return;
                    }

                    for (var m in memory) {
                        server[m] = memory[m];
                    }

                    cb();
                });
        },
        function (cb) {
            if (skip('vms', fields)) {
                cb();
                return;
            }
            self.cacheGetVms(
                function (cacheError, vms) {
                    if (cacheError) {
                        self.logerror(cacheError, 'getting vms');
                        cb(cacheError);
                        return;
                    }

                    server.vms = {};
                    if (vms) {
                        Object.keys(vms).forEach(function (uuid) {
                            try {
                                server.vms[uuid] = JSON.parse(vms[uuid]);
                            } catch (e) {
                                self.log.error(e);
                                cb(e);
                                return;
                            }
                        });
                    }
                    cb();
                });
        }
    ],
    function (error) {
        if (error) {
            callback(error);
            return;
        }
        callback(null, server);
    });
};


/**
 * Update the a servers cache values from an incoming heartbeat message.
 */

ModelServer.prototype.updateCacheFromHeartbeat =
function (heartbeat, callback) {
    var self = this;

    async.waterfall([
        // Update the server's zones cache
        updateCacheZones,

        // Update the server's memory cache
        updateCacheMemory,

        // Update the server's memory cache
        updateCacheStatus
    ],
    function (error) {
        callback(error);
    });

    function updateCacheZones(cb) {
        var vms = {};

        if (heartbeat.hasOwnProperty('vms')) {
            Object.keys(heartbeat.vms).forEach(function (uuid) {
                vms[uuid] = JSON.stringify(heartbeat.vms[uuid]);
            });
        } else {
            heartbeat.zoneStatus.forEach(function (zonerec) {
                var uuid = zonerec[4];
                vms[uuid] = '{}';
            });
        }

        self.cacheSetVms(vms, function (error) {
            if (error) {
                self.log.error('Could not update VMs cache on heartbeat');
                cb(error);
                return;
            }
            cb();
        });
    }

    function updateCacheMemory(cb) {
        if (!heartbeat.hasOwnProperty('meminfo')) {
            cb();
            return;
        }

        self.updateMemoryFromHeartbeat(heartbeat, function () {
            cb();
        });
    }

    function updateCacheStatus(cb) {
        self.cacheSetServerStatus(
            'running',
            function (error) {
                if (error) {
                    self.log.error(
                        error,
                        'Error setting server (%s) status', self.uuid);
                }
                cb();
            });
    }
};


/**
 * Cache server memory usage values in Redis.
 */

ModelServer.prototype.updateMemoryFromHeartbeat =
function (heartbeat, callback) {
    var memoryKeys = [
        ['availrmem_bytes', 'memory_available_bytes'],
        ['arcsize_bytes', 'memory_arc_bytes'],
        ['total_bytes', 'memory_total_bytes'] ];

    var memory = {};

    memoryKeys.forEach(function (keys) {
        memory[keys[1]] = heartbeat.meminfo[keys[0]];
    });

    var serverModel = new ModelServer(this.uuid);
    serverModel.cacheSetMemoryUsage(memory, callback);
};

/**
 * Returns a copy of the server model's internal representation retried from
 * the backend store, or from memory if this object has been previously
 * fetched.
 */
ModelServer.prototype.getRaw = function (callback) {
    var self = this;
    var uuid = self.uuid;
    var server;

    if (self.exists === false) {
        this.log.debug(
            '%s was not found previously, returning negative result', uuid);
        server = clone(self.value);
        callback(null, server);

    }

    if (self.value) {
        this.log.debug('Reusing raw value for %s', uuid);
        server = clone(self.value);
        callback(null, server);
    } else {
        this.log.debug('Fetching server %s from moray', uuid);
        ModelServer.getMoray().getObject(
            buckets.servers.name,
            uuid,
            function (error, obj) {
                if (error && error.name === 'ObjectNotFoundError') {
                    self.exists = false;
                    self.log.error('Server %s not found in moray', uuid);
                    callback();
                    return;
                } else if (error) {
                    self.log.error(error, 'Error fetching server from moray');
                    callback(error);
                    return;
                }
                self.found = true;
                self.exists = true;
                server = clone(obj.value);
                self.value = obj.value;

                server = clone(self.value);
                callback(null, server);
            });
    }
};


ModelServer.updateServerPropertiesFromSysinfo = function (opts) {
    var server = opts.server;
    var sysinfo = opts.sysinfo;

    server.sysinfo = opts.sysinfo;
    server.ram = Number(sysinfo['MiB of Memory']);
    server.current_platform = sysinfo['Live Image'];
    server.headnode = sysinfo['Boot Parameters']['headnode'] === 'true';
    server.boot_platform = sysinfo['Live Image'];
};

/**
 * Create a server object suitable for insertion into Moray from a sysinfo
 * object or heartbeat.
 */

ModelServer.prototype.initialServerValues = function (opts) {
    var server = {};
    var sysinfo = opts.sysinfo;
    var heartbeat = opts.heartbeat;

    server.datacenter = ModelServer.getConfig().datacenter;

    ModelServer.updateServerPropertiesFromSysinfo({
        sysinfo: sysinfo,
        server: server
    });

    server.overprovision_ratio = 1.0;
    server.reservation_ratio = 0.15;
    server.traits = {};
    server.rack_identifier = '';
    server.uuid = sysinfo.UUID;
    server.hostname = sysinfo.Hostname;
    server.reserved = false;

    server.boot_params = opts.boot_params.kernel_args || {};

    server.default_console = opts.boot_params.default_console || 'vga';
    server.serial = opts.boot_params.serial || 'ttyb';
    server.serial_speed = opts.boot_params.serial_speed || 115200;

    if (opts.setup) {
        server.setup = true;
    } else if (sysinfo['SDC Version'] === '7.0' &&
               !sysinfo.hasOwnProperty('Setup'))
    {
        if (sysinfo.hasOwnProperty('Zpool')) {
            server.setup = true;
        } else {
            server.setup = false;
        }
    } else if (sysinfo.hasOwnProperty('Setup')) {
        if (sysinfo['Setup'] === 'true' || sysinfo['Setup'] === true) {
            server.setup = true;
        } else if (sysinfo['Setup'] === 'false' || sysinfo['Setup'] === false) {
            server.setup = false;
        }
    }

    server.setting_up = false;

    if (opts.last_boot) {
        server.last_boot = opts.last_boot;
    }

    if (heartbeat) {
        var meminfo = heartbeat.meminfo;
        if (heartbeat.hasOwnProperty('meminfo'))  {
            server.memory = {
                memory_available_bytes: meminfo.availrmem_bytes,
                memory_arc_bytes: meminfo.arcsize_bytes,
                memory_total_bytes: meminfo.total_bytes
            };
        }
    }

    return server;
};


/**
 * Create a server object in Moray. Use the sysinfo values if they are given in
 * the opts argument. If no sysinfo values are given, do a sysinfo lookup on
 * the server via Ur, and then create the server using those values.
 */

ModelServer.prototype.create = function (opts, callback) {
    var self = this;
    var uuid = this.uuid;
    var sysinfo;
    var server;
    var boot_params;

    async.waterfall([
        function (cb) {
            if (opts.sysinfo) {
                sysinfo = opts.sysinfo;
                cb();
            } else {
                self.log.info('Querying Ur agent for server sysinfo');
                ModelServer.getUr().serverSysinfo(
                    uuid,
                    function (error, returnedSysinfo) {
                    sysinfo = returnedSysinfo;
                    cb();
                });
            }
        },
        function (cb) {
            ModelServer.getBootParamsDefault(
                function (error, params) {
                    boot_params = params;
                    cb();
                });
        }
    ],
    function (error) {
        server = self.initialServerValues({
            boot_params: boot_params,
            heartbeat: opts.heartbeat,
            sysinfo: sysinfo,
            last_boot: opts.last_boot,
            setup: opts.setup
        });

        server.created = (new Date()).toISOString();
        server.status = 'running';

        self.store(
            server,
            function (createError, createdServer) {
                if (createError) {
                    self.log.error(
                        createError,
                        'Error creating server in moray');
                    callback(createError);
                    return;
                }
                self.log.info('Stored server record in moray');

                self.cacheSetServerStatus(
                    server.status,
                    function (statusError) {<commit_msg>CNAPI-237: /servers endpoint was reporting wrong status<commit_after>/*
 * Copyright (c) 2012, Joyent, Inc. All rights reserved.
 *
 * This file contains all the Compute Server logic, used to interface with the
 * server as well as it's stored representation in the backend datastores.
 */

var async = require('async');
var sprintf = require('sprintf').sprintf;
var util = require('util');
var common = require('../common');
var buckets = require('../apis/moray').BUCKETS;
var verror = require('verror');

var ModelBase = require('./base');
var ModelVM = require('./vm');

var PROVISIONER = 'provisioner';

/**
 * RFC 2254 Escaping of filter strings
 * @author [<NAME>](https://github.com/ozten)
 */

function filterEscape(inp) {
    if (typeof (inp) === 'string') {
        var esc = '';
        for (var i = 0; i < inp.length; i++) {
            switch (inp[i]) {
                case '*':
                    esc += '\\2a';
                    break;
                case '(':
                    esc += '\\28';
                    break;
                case ')':
                    esc += '\\29';
                    break;
                case '\\':
                    esc += '\\5c';
                    break;
                case '\0':
                    esc += '\\00';
                    break;
                default:
                    esc += inp[i];
                    break;
            }
        }

        return esc;
    } else {
        return inp;
    }
}


/**
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 *
 * ModelServer encapsulates the logic for manipulating and interacting
 * servers and their back-end storage representation.
 *
 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 */

function ModelServer(uuid) {
    if (!uuid) {
        throw new Error('ModelServer missing uuid parameter');
    }

    this.value = null;
    this.uuid = uuid;

    this.log = ModelServer.getLog();
}


ModelServer.init = function (model) {
    this.model = model;

    Object.keys(ModelBase.staticFn).forEach(function (p) {
        ModelServer[p] = ModelBase.staticFn[p];
    });

    ModelServer.tasks = {};
    ModelServer.log = model.getLog();
};


ModelServer.createProvisionerEventHandler = function (model, jobuuid) {
    var self = this;
    var workflow = ModelServer.getWorkflow();

    return function (taskid, event) {
        if (!jobuuid) {
            return;
        }

        self.log.info(
            'Posting task info (task %s) to workflow jobs endpoint (job %s)',
            taskid, jobuuid);
        workflow.getClient().client.post(
            '/jobs/' + jobuuid + '/info',
            event,
            function (error, req, res, obj) {
                if (error) {
                    self.log.error(
                        error, 'Error posting info to jobs endpoint');
                    return;
                }
                self.log.info(
                    'Posted task info (task %s, job %s)',
                    taskid, jobuuid);
            });
    };
};


/**
 * Return a list of servers matching given criteria.
 */

ModelServer.list = function (params, callback) {
    var self = this;

    var uuid = params.uuid;

    var extras = params.extras || { status: true };

    this.log.debug(params, 'Listing servers');

    var wantFinal = params.wantFinal;

    var filterParams = ['datacenter', 'setup', 'headnode'];
    var filter = '';

    if (Array.isArray(uuid)) {
        var uuidFilter = uuid.map(function (u) {
            return '(uuid=' + u + ')';
        }).join('');
        filter += '(|' + uuidFilter + ')';
    } else if (uuid) {
        filter += '(uuid=' + uuid + ')';
    } else {
        filter += '(uuid=*)';
    }

    var paramsFilter = [];
    filterParams.forEach(function (p) {
        if (!params.hasOwnProperty(p) || typeof (params[p]) === 'undefined') {
            return;
        }

        if (!buckets.servers.bucket.index[p]) {
            return;
        }

        if (buckets.servers.bucket.index[p].type === 'string') {
            paramsFilter.push(sprintf('(%s=%s)', p, filterEscape(params[p])));
        } else if (buckets.servers.bucket.index[p].type === 'number') {
            paramsFilter.push(sprintf('(%s=%s)', p, filterEscape(params[p])));
        } else if (buckets.servers.bucket.index[p].type === 'boolean') {
            paramsFilter.push(sprintf('(%s=%s)', p, filterEscape(params[p])));
        }
    });

    paramsFilter.push('!(uuid=default)');

    if (paramsFilter.length > 1) {
        filter = sprintf('(&%s(&%s))', filter, paramsFilter.join(''));
    } else if (paramsFilter.length === 1) {
        filter = sprintf('(&%s%s)', filter, paramsFilter[0]);
    }

    var moray = ModelServer.getMoray();

    var findOpts = {
            sort: {
                attribute: 'uuid',
                order: 'ASC'
            }
        };

    ['limit', 'offset'].forEach(function (f) {
        if (params.hasOwnProperty(f)) {
            findOpts[f] = params[f];
        }
    });

    var req = moray.findObjects(
        buckets.servers.name,
        filter,
        findOpts);

    var servers = [];

    req.on('error', onError);
    req.on('record', onRecord);
    req.on('end', processResults);

    function onError(error) {
        self.log.error(error, 'Error retriving results');
        callback(error);
    }

    function onRecord(server) {
        servers.push(server.value);
    }

    function processResults() {
        if (!wantFinal) {
            callback(null, servers);
            return;
        }

        async.map(
            servers,
            function (server, cb) {
                var serverModel = new ModelServer(server.uuid);
                serverModel.setRaw(server);

                serverModel.getFinal(extras, function (error, s) {
                    cb(null, s);
                });
            },
            function (error, results) {
                callback(null, results);
            });
    }
};


/**
 * Initiate a workflow, which can may be can be added to which is run whenever
 * a new server starts up and sends its sysinfo payload via Ur.
 */

ModelServer.beginSysinfoWorkflow = function (sysinfo, callback) {
    var self = this;

    var uuid = sysinfo.UUID;

    var params = {
        sysinfo: sysinfo,
        server_uuid: uuid,
        target: uuid,
        admin_uuid: ModelServer.getConfig().adminUuid
    };

    self.log.info('Instantiating server-sysinfo workflow');
    ModelServer.getWorkflow().getClient().createJob(
        'server-sysinfo',
        params,
        function (error, job) {
            if (error) {
                self.log.error('Error in workflow: %s', error.message);
                if (callback) {
                    callback(error);
                }
                return;
            }
            if (callback) {
                callback();
            }
        });
};

/**
 * Creates an object that will contain default values for new servers.
 */
ModelServer.setDefaultServer = function (values, callback) {
    var self = this;

    var defaultServer = {
        uuid: 'default',
        boot_platform: 'latest',
        default_console: 'vga',
        serial: 'ttyb',
        serial_speed: 115200,
        boot_params: {}
    };

    if (arguments.length === 2) {
        defaultServer = {
            uuid: 'default'
        };
        for (var k in values) {
            defaultServer[k] = values[k];
        }
    } else {
        defaultServer = {
            uuid: 'default',
            boot_platform: 'latest',
            default_console: 'vga',
            serial: 'ttyb',
            serial_speed: 115200,
            boot_params: {}
        };
        callback = arguments[0];
    }

    var moray = ModelServer.getMoray();

    moray.putObject(
        buckets.servers.name,
        'default',
        defaultServer,
        function (putError) {
            if (putError) {
                self.log.error('Could not store default server');
                callback(
                    verror.VError(putError, 'failed to store default server'));
                return;
            }
            callback();
        });
};

ModelServer.updateDefaultServer = function (values, callback) {
    var self = this;
    var moray = ModelServer.getMoray();

    moray.getObject(
        buckets.servers.name,
        'default',
        function (error, obj) {
            if (error) {
                self.log.error(error, 'Could not store default server');
                callback(verror.VError(
                    error, 'failed to store default server'));
                return;
            }

            var server = obj.value;
            if (!server.boot_params) {
                server.boot_params = {};
            }
            var boot_params = values.boot_params;

            for (var k in boot_params) {
                if (boot_params[k] === null) {
                    delete server.boot_params[k];
                    continue;
                }
                server.boot_params[k] = boot_params[k];
            }

            var names = ['default_console', 'serial', 'serial_speed'];

            names.forEach(function (n) {
                if (values[n] === null) {
                    delete server[n];
                    return;
                }
                server[n] = values[n] || server[n];
            });

            if (values.boot_platform) {
                server.boot_platform = values.boot_platform;
            }

            moray.putObject(
                buckets.servers.name,
                'default',
                server,
                function (putError) {
                    if (putError) {
                        self.log.error('Could not store default server');
                        callback(verror.VError(
                            putError, 'failed to store default server'));
                        return;
                    }
                    callback();
                });
        });
};

/**
 * Execute a command on a particular server via Ur.
 */

ModelServer.prototype.invokeUrScript =
function (script, params, callback) {
    var self = this;
    var uuid = this.uuid;

    var opts = {
        uuid: uuid,
        message: {
            type: 'script',
            script: script,
            args: params.args || [],
            env: params.env || {}
        }
    };
    self.log.info('Sending compute node %s script', uuid);

    ModelServer.getUr().execute(opts, function (err, stdout, stderr) {
        if (err) {
            self.log.error('Error raised by ur when ' +
                'running script: ' + err.message);
        }

        callback(err, stdout, stderr);
        return;
    });
};

/**
 * Reboot a server.
 */

ModelServer.prototype.reboot =
function (callback) {
    var self = this;

    self.invokeUrScript(
        '#!/bin/bash\nexit 113', {}, function (error) {
            callback(error);
        });
};


/**
 * Format an error message with
 */

ModelServer.prototype.errorFmt = function (str) {
    return sprintf('Error (server=%s): %s', this.uuid, str);
};


/**
 * Logs an error message along with the relevant error object.
 */

ModelServer.prototype.logerror = function (error, str) {
    var self = this;
    this.log.error(error, self.errorFmt(str));
};


/**
 * Fetches and returns the server's cached values and merges them with the raw
 * internal values.
 */

ModelServer.prototype.applyCachedValues = function (fields, callback) {
    var self = this;

    var server = clone(self.value);

    function isBoolean(a) {
        return !!a === a;
    }

    function skip(f, fo) {
        var ret = (
            !isBoolean(fo[f]) ||
            !fo[f]);
        return ret;
    }

    async.parallel([
        function (cb) {
            if (skip('status', fields)) {
                cb();
                return;
            }
            self.cacheGetServerStatus(
                function (statusError, status) {
                    if (statusError) {
                        self.logerror(
                            statusError,
                            'fetching server status');

                        cb(statusError);
                        return;
                    }

                    server.status = status || 'unknown';
                    self.log.debug(
                        'Status for %s was %s', self.uuid, server.status);
                    cb();
                });
        },
        function (cb) {
            if (skip('memory', fields)) {
                cb();
                return;
            }
            self.cacheGetMemoryUsage(
                function (cacheError, memory) {
                    if (cacheError) {
                        self.logerror(
                            cacheError,
                            'fetching memory values');

                        cb(cacheError);
                        return;
                    }

                    for (var m in memory) {
                        server[m] = memory[m];
                    }

                    cb();
                });
        },
        function (cb) {
            if (skip('vms', fields)) {
                cb();
                return;
            }
            self.cacheGetVms(
                function (cacheError, vms) {
                    if (cacheError) {
                        self.logerror(cacheError, 'getting vms');
                        cb(cacheError);
                        return;
                    }

                    server.vms = {};
                    if (vms) {
                        Object.keys(vms).forEach(function (uuid) {
                            try {
                                server.vms[uuid] = JSON.parse(vms[uuid]);
                            } catch (e) {
                                self.log.error(e);
                                cb(e);
                                return;
                            }
                        });
                    }
                    cb();
                });
        }
    ],
    function (error) {
        if (error) {
            callback(error);
            return;
        }
        callback(null, server);
    });
};


/**
 * Update the a servers cache values from an incoming heartbeat message.
 */

ModelServer.prototype.updateCacheFromHeartbeat =
function (heartbeat, callback) {
    var self = this;

    async.waterfall([
        // Update the server's zones cache
        updateCacheZones,

        // Update the server's memory cache
        updateCacheMemory,

        // Update the server's memory cache
        updateCacheStatus
    ],
    function (error) {
        callback(error);
    });

    function updateCacheZones(cb) {
        var vms = {};

        if (heartbeat.hasOwnProperty('vms')) {
            Object.keys(heartbeat.vms).forEach(function (uuid) {
                vms[uuid] = JSON.stringify(heartbeat.vms[uuid]);
            });
        } else {
            heartbeat.zoneStatus.forEach(function (zonerec) {
                var uuid = zonerec[4];
                vms[uuid] = '{}';
            });
        }

        self.cacheSetVms(vms, function (error) {
            if (error) {
                self.log.error('Could not update VMs cache on heartbeat');
                cb(error);
                return;
            }
            cb();
        });
    }

    function updateCacheMemory(cb) {
        if (!heartbeat.hasOwnProperty('meminfo')) {
            cb();
            return;
        }

        self.updateMemoryFromHeartbeat(heartbeat, function () {
            cb();
        });
    }

    function updateCacheStatus(cb) {
        self.cacheSetServerStatus(
            'running',
            function (error) {
                if (error) {
                    self.log.error(
                        error,
                        'Error setting server (%s) status', self.uuid);
                }
                cb();
            });
    }
};


/**
 * Cache server memory usage values in Redis.
 */

ModelServer.prototype.updateMemoryFromHeartbeat =
function (heartbeat, callback) {
    var memoryKeys = [
        ['availrmem_bytes', 'memory_available_bytes'],
        ['arcsize_bytes', 'memory_arc_bytes'],
        ['total_bytes', 'memory_total_bytes'] ];

    var memory = {};

    memoryKeys.forEach(function (keys) {
        memory[keys[1]] = heartbeat.meminfo[keys[0]];
    });

    var serverModel = new ModelServer(this.uuid);
    serverModel.cacheSetMemoryUsage(memory, callback);
};

/**
 * Returns a copy of the server model's internal representation retried from
 * the backend store, or from memory if this object has been previously
 * fetched.
 */
ModelServer.prototype.getRaw = function (callback) {
    var self = this;
    var uuid = self.uuid;
    var server;

    if (self.exists === false) {
        this.log.debug(
            '%s was not found previously, returning negative result', uuid);
        server = clone(self.value);
        callback(null, server);

    }

    if (self.value) {
        this.log.debug('Reusing raw value for %s', uuid);
        server = clone(self.value);
        callback(null, server);
    } else {
        this.log.debug('Fetching server %s from moray', uuid);
        ModelServer.getMoray().getObject(
            buckets.servers.name,
            uuid,
            function (error, obj) {
                if (error && error.name === 'ObjectNotFoundError') {
                    self.exists = false;
                    self.log.error('Server %s not found in moray', uuid);
                    callback();
                    return;
                } else if (error) {
                    self.log.error(error, 'Error fetching server from moray');
                    callback(error);
                    return;
                }
                self.found = true;
                self.exists = true;
                server = clone(obj.value);
                self.value = obj.value;

                server = clone(self.value);
                callback(null, server);
            });
    }
};


ModelServer.updateServerPropertiesFromSysinfo = function (opts) {
    var server = opts.server;
    var sysinfo = opts.sysinfo;

    server.sysinfo = opts.sysinfo;
    server.ram = Number(sysinfo['MiB of Memory']);
    server.current_platform = sysinfo['Live Image'];
    server.headnode = sysinfo['Boot Parameters']['headnode'] === 'true';
    server.boot_platform = sysinfo['Live Image'];
};

/**
 * Create a server object suitable for insertion into Moray from a sysinfo
 * object or heartbeat.
 */

ModelServer.prototype.initialServerValues = function (opts) {
    var server = {};
    var sysinfo = opts.sysinfo;
    var heartbeat = opts.heartbeat;

    server.datacenter = ModelServer.getConfig().datacenter;

    ModelServer.updateServerPropertiesFromSysinfo({
        sysinfo: sysinfo,
        server: server
    });

    server.overprovision_ratio = 1.0;
    server.reservation_ratio = 0.15;
    server.traits = {};
    server.rack_identifier = '';
    server.uuid = sysinfo.UUID;
    server.hostname = sysinfo.Hostname;
    server.reserved = false;

    server.boot_params = opts.boot_params.kernel_args || {};

    server.default_console = opts.boot_params.default_console || 'vga';
    server.serial = opts.boot_params.serial || 'ttyb';
    server.serial_speed = opts.boot_params.serial_speed || 115200;

    if (opts.setup) {
        server.setup = true;
    } else if (sysinfo['SDC Version'] === '7.0' &&
               !sysinfo.hasOwnProperty('Setup'))
    {
        if (sysinfo.hasOwnProperty('Zpool')) {
            server.setup = true;
        } else {
            server.setup = false;
        }
    } else if (sysinfo.hasOwnProperty('Setup')) {
        if (sysinfo['Setup'] === 'true' || sysinfo['Setup'] === true) {
            server.setup = true;
        } else if (sysinfo['Setup'] === 'false' || sysinfo['Setup'] === false) {
            server.setup = false;
        }
    }

    server.setting_up = false;

    if (opts.last_boot) {
        server.last_boot = opts.last_boot;
    }

    if (heartbeat) {
        var meminfo = heartbeat.meminfo;
        if (heartbeat.hasOwnProperty('meminfo'))  {
            server.memory = {
                memory_available_bytes: meminfo.availrmem_bytes,
                memory_arc_bytes: meminfo.arcsize_bytes,
                memory_total_bytes: meminfo.total_bytes
            };
        }
    }

    return server;
};


/**
 * Create a server object in Moray. Use the sysinfo values if they are given in
 * the opts argument. If no sysinfo values are given, do a sysinfo lookup on
 * the server via Ur, and then create the server using those values.
 */

ModelServer.prototype.create = function (opts, callback) {
    var self = this;
    var uuid = this.uuid;
    var sysinfo;
    var server;
    var boot_params;

    async.waterfall([
        function (cb) {
            if (opts.sysinfo) {
                sysinfo = opts.sysinfo;
                cb();
            } else {
                self.log.info('Querying Ur agent for server sysinfo');
                ModelServer.getUr().serverSysinfo(
                    uuid,
                    function (error, returnedSysinfo) {
                    sysinfo = returnedSysinfo;
                    cb();
                });
            }
        },
        function (cb) {
            ModelServer.getBootParamsDefault(
                function (error, params) {
                    boot_params = params;
                    cb();
                });
        }
    ],
    function (error) {
        server = self.initialServerValues({
            boot_params: boot_params,
            heartbeat: opts.heartbeat,
            sysinfo: sysinfo,
            last_boot: opts.last_boot,
            setup: opts.setup
        });

        server.created = (new Date()).toISOString();

        self.store(
            server,
            function (createError, createdServer) {
                if (createError) {
                    self.log.error(
                        createError,
                        'Error creating server in moray');
                    callback(createError);
                    return;
                }
                self.log.info('Stored server record in moray');

                self.cacheSetServerStatus(
                    server.status,
                    function (statusError) {
{'fraction_non_alphanumeric': 0.12307795011014931, 'fraction_numerical': 0.002470014909099001, 'mean_word_length': 1.4527889968344068, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 7, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 24, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3352765', 'n_tokens_mistral': 12521, 'n_tokens_neox': 11918, 'n_words': 2812}