Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 19d39271fa
Fetching contributors…

Cannot retrieve contributors at this time

executable file 353 lines (296 sloc) 8.647 kb
#!/usr/bin/env node
/*
* smfgen: generates an SMF manifest from a JSON description of the service. See
* emitManifest below for details.
*
* This tool is still experimental. It's only intended to generate simple
* manifests. For more details, see smf(5).
*/
var mod_assert = require('assert');
var header_comment = [
'',
' Manifest automatically generated by smfgen.',
''
].join('\n');
function checkAndApplyContext(prefix, conf, applyto)
{
var s = (prefix ? prefix + '.' : '');
if (conf.hasOwnProperty('user')) {
mod_assert.equal(typeof (conf.user), 'string',
'"' + s + 'user" property must be a string');
applyto.user = conf.user;
}
if (conf.hasOwnProperty('group')) {
mod_assert.equal(typeof (conf.group), 'string',
'"' + s + 'group" property must be a string');
applyto.group = conf.group;
}
if (conf.hasOwnProperty('environment')) {
mod_assert.equal(typeof (conf.environment), 'object',
'"' + s + 'environment" property must be an object');
applyto.environment = conf.environment;
}
if (conf.hasOwnProperty('privileges')) {
mod_assert.ok(Array.isArray(conf.privileges),
'"' + s + 'privileges" property must be an array');
applyto.privileges = conf.privileges;
}
if (conf.hasOwnProperty('working_directory')) {
mod_assert.equal(typeof (conf.working_directory), 'string',
'"' + s + 'working_directory" property must be a string');
applyto.working_directory = conf.working_directory;
}
if (conf.hasOwnProperty('exec')) {
mod_assert.equal(typeof (conf.exec), 'string',
'"' + s + 'exec" property must be a string');
applyto.exec = conf.exec;
}
if (conf.hasOwnProperty('timeout')) {
mod_assert.equal(typeof (conf.timeout), 'number',
'"' + s + 'timeout" property must be a number');
applyto.timeout = conf.timeout;
}
}
function shouldEmitMethodContext(conf)
{
var lookfor = ['user', 'group', 'privileges', 'environment',
'working_directory'];
for (var i = 0; i < lookfor.length; i++) {
if (conf.hasOwnProperty(lookfor[i]))
return (true);
}
return (false);
}
function emitMethodContext(xml, conf)
{
var attrs = {};
if (conf.working_directory)
attrs.working_directory = conf.working_directory;
xml.emitStart('method_context', attrs);
attrs = {};
if (conf.user)
attrs.user = conf.user;
if (conf.group)
attrs.group = conf.group;
if (conf.privileges)
attrs.privileges = conf.privileges.join(',');
if (Object.keys(attrs).length > 0)
xml.emitEmpty('method_credential', attrs);
if (conf.environment) {
xml.emitStart('method_environment');
Object.keys(conf.environment).forEach(function (k) {
var v = conf.environment[k];
xml.emitEmpty('envvar', { name: k, value: v });
});
xml.emitEnd('method_environment');
}
xml.emitEnd('method_context');
}
function emitMethod(xml, conf)
{
var attrs = {
type: 'method',
name: conf.name,
exec: conf.exec,
timeout_seconds: conf.timeout
};
if (shouldEmitMethodContext(conf)) {
xml.emitStart('exec_method', attrs);
emitMethodContext(xml, conf);
xml.emitEnd('exec_method');
} else {
xml.emitEmpty('exec_method', attrs);
}
}
/*
* Emit an SMF manifest for the service described by the given JSON. The
* expected structure of the JSON stream is specified in README.md.
*
* Note that for backwards compatibility we also accept a string in place of
* the method object for 'start' and 'stop', which is assumed to be the 'exec'
* value for that method.
*/
function emitManifest(stream, conf)
{
var xml = new XmlEmitter(stream);
var deps = [ 'svc:/milestone/multi-user:default' ];
var fmri, i;
/* default values for start/stop methods */
var start = { name: 'start', timeout: 10 };
var stop = { name: 'stop', timeout: 30, exec: ':kill' };
mod_assert.ok(conf.hasOwnProperty('ident') &&
typeof (conf['ident']) == 'string',
'"ident" property must be a string');
mod_assert.ok(conf.hasOwnProperty('label') &&
typeof (conf['label']) == 'string',
'"label" property must be a string');
/*
* Apply the global versions of method context and exec from the root
* of the config object first, then override them if more specific
* options exist for a particular method.
*/
checkAndApplyContext(null, conf, start);
checkAndApplyContext(null, conf, stop);
if (conf.hasOwnProperty('start')) {
if (typeof (conf.start) === 'string') {
start.exec = conf.start;
} else {
checkAndApplyContext('start', conf.start, start);
}
}
mod_assert.ok(start.hasOwnProperty('exec') &&
typeof (start.exec) === 'string',
'must provide either "exec" or "start.exec" property as a string');
start.exec += ' &';
if (conf.hasOwnProperty('stop')) {
if (typeof (conf.stop) === 'string')
stop.exec = conf.stop;
else
checkAndApplyContext('stop', conf.stop, stop);
}
if (conf.hasOwnProperty('dependencies')) {
mod_assert.ok(Array.isArray(conf['dependencies']),
'"dependencies" property must be an array');
conf['dependencies'].forEach(function (dep) {
mod_assert.equal(typeof (dep), 'string');
deps.push(dep);
});
}
if (conf.hasOwnProperty('category')) {
mod_assert.equal(typeof (conf['category']), 'string',
'"category" must be a string');
fmri = conf['category'] + '/' + conf['ident'];
} else {
fmri = 'application/' + conf['ident'];
}
xml.emitDoctype('service_bundle', 'SYSTEM',
'/usr/share/lib/xml/dtd/service_bundle.dtd.1');
xml.emitComment(header_comment);
xml.emitStart('service_bundle', {
'type': 'manifest',
/* JSSTYLED */
'name': fmri.replace(/\//g, '-')
});
xml.emitStart('service', {
'name': fmri,
'type': 'service',
'version': '1'
});
xml.emitEmpty('create_default_instance', { 'enabled': 'true' });
i = 0;
deps.forEach(function (dep) {
xml.emitStart('dependency', {
'name': 'dep' + i++,
'grouping': 'require_all',
'restart_on': 'error',
'type': 'service'
});
xml.emitEmpty('service_fmri', { 'value': dep });
xml.emitEnd('dependency');
});
emitMethod(xml, start);
emitMethod(xml, stop);
xml.emitStart('template');
xml.emitStart('common_name');
xml.emitStart('loctext', {
'xml:lang': 'C'
}, { 'bare': true });
xml.emitCData(conf['label']);
xml.emitEnd('loctext', { 'bare': true });
xml.emitEnd('common_name');
xml.emitEnd('template');
xml.emitEnd('service');
xml.emitEnd('service_bundle');
}
/*
* Basic interface for emitting well-formed XML. This isn't bulletproof, but it
* does escape values (not tags or keys) and checks for basic errors.
*/
function XmlEmitter(stream)
{
this.xe_stream = stream;
this.xe_stack = [];
}
XmlEmitter.prototype.emitDoctype = function (name, type, path)
{
this.xe_stream.write('<?xml version="1.0"?>\n');
this.xe_stream.write('<!DOCTYPE ' + name + ' ' + type + ' "' +
path + '">\n');
};
XmlEmitter.prototype.escape = function (str)
{
/* BEGIN JSSTYLED */
return (str.toString().replace(/&/g, '&amp;').
replace(/</g, '&lt;').
replace(/>/g, '&gt;').
replace(/"/g, '&quot;'));
/* END JSSTYLED */
};
XmlEmitter.prototype.emitIndent = function ()
{
var str = '';
var i;
for (i = 0; i < this.xe_stack.length; i++)
str += ' ';
this.xe_stream.write(str);
};
XmlEmitter.prototype.emitEmpty = function (name, attrs)
{
this.emitIndent();
this.xe_stream.write('<' + name + ' ');
this.emitAttrs(attrs);
this.xe_stream.write('/>\n');
};
XmlEmitter.prototype.emitAttrs = function (attrs)
{
var key;
if (!attrs)
return;
for (key in attrs)
this.xe_stream.write(key + '=\"' +
this.escape(attrs[key]) + '\" ');
};
XmlEmitter.prototype.emitStart = function (name, attrs, opts)
{
this.emitIndent();
this.xe_stack.push(name);
this.xe_stream.write('<' + name + ' ');
this.emitAttrs(attrs);
this.xe_stream.write('>');
if (!opts || !opts['bare'])
this.xe_stream.write('\n');
};
XmlEmitter.prototype.emitEnd = function (name, opts)
{
var check = this.xe_stack.pop();
mod_assert.equal(name, check);
if (!opts || !opts['bare'])
this.emitIndent();
this.xe_stream.write('</' + name + '>\n');
};
XmlEmitter.prototype.emitCData = function (data)
{
this.xe_stream.write(this.escape(data));
};
XmlEmitter.prototype.emitComment = function (content)
{
this.xe_stream.write('<!-- ' + content + ' -->\n');
};
function main()
{
var stdin, data, json;
data = '';
stdin = process.openStdin();
stdin.setEncoding('utf8');
stdin.on('data', function (chunk) { data += chunk; });
stdin.on('end', function () {
try {
json = JSON.parse(data);
emitManifest(process.stdout, json);
} catch (ex) {
console.error('smfgen: ' + ex.message);
process.exit(1);
}
});
}
main();
Jump to Line
Something went wrong with that request. Please try again.