├── LICENSE
├── README.md
├── package.json
├── smfgen
└── test
├── assets
├── basic-manifest.json
├── basic-manifest.xml
└── nginx-cli-manifest.xml
├── cli-test.js
└── module-test.js
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2013, Joyent, Inc. All rights reserved.
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # smfgen
2 |
3 | This tool generates an SMF manifest from a JSON description of the service.
4 | It's only intended to generate simple manifests. For more details, see _smf(5)_.
5 |
6 | This tool is still experimental.
7 |
8 | [sudo] npm install -g smfgen
9 |
10 |
11 | # JSON Input
12 |
13 | Emits an SMF manifest for the service described by the given JSON:
14 |
15 | | Property | Description
16 | |----------------|----------------------------------------------------------
17 | | ident | The SMF identifier for the service. The full SMF identifier (FMRI) will be constructed from the category and identifier.
18 | | [category] | Service category (default: "application").
19 | | label | The human-readable name of the service.
20 | | [dependencies] | Array of service FMRIs that must be online before this service is started.
21 | | start | The method object that describes how to start the service. (See below.)
22 | | [stop] | The method object that describes how to stop the service. (See below.)
23 | | [refresh] | The method object that describes how to refresh the service. (See below.)
24 | | [enabled] | Whether to start the service automatically (default: true)
25 |
26 | Both the `start` and `stop` properties in the JSON should have object values that
27 | describe the context of the method. A method context consists of these
28 | properties:
29 |
30 | | Property | Description
31 | |----------------------|----------------------------------------------------
32 | | exec | The script to run for this method. Defaults to `:kill` for the `stop` method. This invocation may run the service synchronously or in the background. Note that this mechanism does not allow you to use SMF's built in timeout for service startup, since it doesn't know when the service has actually started.
33 | | [user] | Run the script for this method as this user.
34 | | [group] | Run the script for this method as this group.
35 | | [environment] | A hash of environment variables for this method script.
36 | | [privileges] | An array of RBAC privilege names. This method will be run with this privilege set. See also: _privileges(5)_.
37 | | [working\_directory] | Use this working directory when invoking the method script.
38 | | [timeout] | The number of seconds the method may run before it is considered timed out and aborted. Defaults to 10 for `start` and `refresh` and 30 for `stop`.
39 |
40 | A set of example methods might look like this:
41 |
42 | ```json
43 | {
44 | "start": {
45 | "user": "webservd", "group": "webservd",
46 | "exec": "/opt/pkg/sbin/apachectl start"
47 | },
48 | "stop": {
49 | "timeout": 120
50 | }
51 | }
52 | ```
53 |
54 | Note that you may also specify the properties that describe the context of a
55 | method at the top level of the JSON. Top-level properties apply to all
56 | methods, but may also be overridden in a specific method. For example:
57 |
58 | ```json
59 | {
60 | "user": "webservd", "group": "webservd",
61 | "exec": "/opt/pkg/sbin/apachectl %m",
62 | "timeout": 10,
63 | "stop": {
64 | "timeout": 120
65 | }
66 | }
67 | ```
68 |
69 | For compatibility with earlier versions of smfgen, the program will accept
70 | a string in place of an object for the `start`, `stop`, and `refresh` methods.
71 | This string will be assumed to be the `exec` property of that method.
72 |
73 |
74 | ## Example
75 |
76 | $ cat bapi.json
77 |
78 | ``` json
79 | {
80 | "ident": "bapi",
81 | "label": "Boilerplate API",
82 | "start": "node bapi.js"
83 | }
84 | ```
85 |
86 | $ ./smfgen < bapi.json
87 |
88 | ``` xml
89 |
90 |
91 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 | Boilerplate API
105 |
106 |
107 |
108 |
109 | ```
110 |
111 | # CLI Input
112 |
113 | The following options can be specified over the CLI:
114 |
115 | - `-c`, `--category` becomes `data.category`
116 | - `-d`, `--cwd` becomes `data.working_directory`
117 | - `-D`, `--dependency` appends to `data.dependencies`
118 | - `-e`, `--env` appends to `data.environment`
119 | - `-g`, `--group` becomes `data.group`
120 | - `-i`, `--ident` becomes `data.ident`
121 | - `-l`, `--label` becomes `data.label`
122 | - `-p`, `--privilege` appends to `data.privileges`
123 | - `-r`, `--refresh` becomes `data.refresh.exec`
124 | - `-s`, `--start` becomes `data.start.exec`
125 | - `-S`, `--stop` becomes `data.stop.exec`
126 | - `-t`, `--timeout` becomes `data.timeout`
127 | - `-u`, `--user` becomes `data.user`
128 |
129 | ## Example
130 |
131 | $ ./smfgen -i nginx -l 'NGINX Web Server' \
132 | -s 'nginx -d' -d /var/www \
133 | -u nobody -g other \
134 | -p basic -p net_privaddr \
135 | -eHOME=/var/tmp -ePATH=/bin:/usr/bin
136 |
137 | ``` xml
138 |
139 |
140 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 | NGINX Web Server
179 |
180 |
181 |
182 |
183 | ```
184 |
185 | # Assumptions
186 |
187 | This tool makes a ton of assumptions about your service:
188 |
189 | * Your service is a contract-model service in SMF. That means SMF should consider the service failed (and potentially restart it) if:
190 | * all processes in the service exit, OR
191 | * any process in the service produces a core dump, OR
192 | * any process outside the service sends any service process a fatal signal
193 | See _svc.startd(1M)_ for details.
194 | * Your service is an application which depends on system services like the filesystem and network. This tool wouldn't work for system services implementing any of that functionality.
195 | * If you specify any additional dependencies (like other services of yours), that means your service should not be started until those other services are online. However, if those services restart, your service will not be restarted.
196 | * You only intend to have one instance of your service.
197 | * SMF provides a mechanism for timing out the "start" operation. But for simplicity, this tool always runs your start script in the background, so as far as SMF sees it starts almost instantly. If you want to detect "start" timeout, you must implement a start method that returns exactly when your program has started providing service (e.g., opened its server socket), and you'll have to write your own manifest rather than use this tool.
198 | * By default, the "stop" method just kills all processes in this service, which includes all processes forked by the initial "start" script. You can override this with a "stop" script, but you should use the default if that script is only going to kill processes. There's a default 30 second timeout on the stop script, so the processes must exit within about 30 seconds of receiving the signal.
199 | * The service does not use SMF to store configuration properties.
200 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "smfgen",
3 | "version": "1.2.0",
4 | "description": "Generate an SMF manifest from a JSON or CLI description of the service",
5 | "author": "Dave Pacheco ",
6 | "repository": {
7 | "url": "https://github.com/joyent/smfgen",
8 | "type": "git"
9 | },
10 | "main": "./smfgen",
11 | "bin": {
12 | "smfgen": "./smfgen"
13 | },
14 | "license": "MIT",
15 | "keywords": [
16 | "illumos",
17 | "smartos",
18 | "smf",
19 | "smfgen"
20 | ],
21 | "dependencies": {
22 | "assert-plus": "^1.0.0",
23 | "posix-getopt": "^1.2.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/smfgen:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /*
4 | * smfgen: generates an SMF manifest from a JSON description of the service. See
5 | * emitManifest below for details.
6 | *
7 | * This tool is still experimental. It's only intended to generate simple
8 | * manifests. For more details, see smf(5).
9 | */
10 |
11 | var util = require('util');
12 |
13 | var assert = require('assert-plus');
14 |
15 | var PROGNAME = require('./package').name;
16 |
17 | var HEADER_COMMENT = [
18 | '',
19 | util.format(' Manifest automatically generated by %s.', PROGNAME),
20 | ''
21 | ].join('\n');
22 |
23 | function checkAndApplyContext(prefix, conf, applyto)
24 | {
25 | var s = (prefix ? prefix + '.' : '');
26 |
27 | if (conf.hasOwnProperty('user')) {
28 | assert.string(conf.user, s + 'user');
29 | applyto.user = conf.user;
30 | }
31 |
32 | if (conf.hasOwnProperty('group')) {
33 | assert.string(conf.group, s + 'group');
34 | applyto.group = conf.group;
35 | }
36 |
37 | if (conf.hasOwnProperty('environment')) {
38 | assert.object(conf.environment, s + 'environment');
39 | applyto.environment = conf.environment;
40 | }
41 |
42 | if (conf.hasOwnProperty('privileges')) {
43 | assert.arrayOfString(conf.privileges, s + 'privileges');
44 | applyto.privileges = conf.privileges;
45 | }
46 |
47 | if (conf.hasOwnProperty('working_directory')) {
48 | assert.string(conf.working_directory, s + 'working_directory');
49 | applyto.working_directory = conf.working_directory;
50 | }
51 |
52 | if (conf.hasOwnProperty('exec')) {
53 | assert.string(conf.exec, s + 'exec');
54 | applyto.exec = conf.exec;
55 | }
56 |
57 | if (conf.hasOwnProperty('timeout')) {
58 | assert.number(conf.timeout, s + 'timeout');
59 | applyto.timeout = conf.timeout;
60 | }
61 |
62 | if (conf.hasOwnProperty('enabled')) {
63 | assert.bool(conf.enabled, s + 'enabled');
64 | applyto.enabled = conf.enabled;
65 | }
66 | }
67 |
68 | function shouldEmitMethodContext(conf)
69 | {
70 | var lookfor = ['user', 'group', 'privileges', 'environment',
71 | 'working_directory'];
72 | for (var i = 0; i < lookfor.length; i++) {
73 | if (conf.hasOwnProperty(lookfor[i]))
74 | return (true);
75 | }
76 | return (false);
77 | }
78 |
79 | function emitMethodContext(xml, conf)
80 | {
81 | var attrs = {};
82 | if (conf.working_directory)
83 | attrs.working_directory = conf.working_directory;
84 | xml.emitStart('method_context', attrs);
85 |
86 | attrs = {};
87 | if (conf.user)
88 | attrs.user = conf.user;
89 | if (conf.group)
90 | attrs.group = conf.group;
91 | if (conf.privileges)
92 | attrs.privileges = conf.privileges.join(',');
93 | if (Object.keys(attrs).length > 0)
94 | xml.emitEmpty('method_credential', attrs);
95 |
96 | if (conf.environment) {
97 | xml.emitStart('method_environment');
98 | Object.keys(conf.environment).forEach(function (k) {
99 | var v = conf.environment[k];
100 | xml.emitEmpty('envvar', { name: k, value: v });
101 | });
102 | xml.emitEnd('method_environment');
103 | }
104 |
105 | xml.emitEnd('method_context');
106 | }
107 |
108 | function emitMethod(xml, conf)
109 | {
110 | var attrs = {
111 | type: 'method',
112 | name: conf.name,
113 | exec: conf.exec,
114 | timeout_seconds: conf.timeout
115 | };
116 | if (shouldEmitMethodContext(conf)) {
117 | xml.emitStart('exec_method', attrs);
118 | emitMethodContext(xml, conf);
119 | xml.emitEnd('exec_method');
120 | } else {
121 | xml.emitEmpty('exec_method', attrs);
122 | }
123 | }
124 |
125 | /*
126 | * Emit an SMF manifest for the service described by the given JSON. The
127 | * expected structure of the JSON stream is specified in README.md.
128 | *
129 | * Note that for backwards compatibility we also accept a string in place of
130 | * the method object for 'start', 'stop', and 'refresh', which is assumed to be
131 | * the 'exec' value for that method.
132 | */
133 | function emitManifest(stream, conf)
134 | {
135 | var xml = new XmlEmitter(stream);
136 | var deps = [ 'svc:/milestone/multi-user:default' ];
137 | var fmri, i;
138 |
139 | /* default values for start/stop methods */
140 | var start = { name: 'start', timeout: 10 };
141 | var stop = { name: 'stop', timeout: 30, exec: ':kill' };
142 | var refresh = { name: 'refresh', timeout: 10, exec: ':true' };
143 |
144 | assert.string(conf.ident, 'ident');
145 | assert.string(conf.label, 'label');
146 |
147 | /*
148 | * Apply the global versions of method context and exec from the root
149 | * of the config object first, then override them if more specific
150 | * options exist for a particular method.
151 | */
152 | checkAndApplyContext(null, conf, start);
153 | checkAndApplyContext(null, conf, stop);
154 | checkAndApplyContext(null, conf, refresh);
155 |
156 | if (conf.hasOwnProperty('start')) {
157 | if (typeof (conf.start) === 'string') {
158 | start.exec = conf.start;
159 | } else {
160 | checkAndApplyContext('start', conf.start, start);
161 | }
162 | }
163 | assert.string(start.exec, 'start.exec');
164 | start.exec += ' &';
165 |
166 | if (conf.hasOwnProperty('stop')) {
167 | if (typeof (conf.stop) === 'string')
168 | stop.exec = conf.stop;
169 | else
170 | checkAndApplyContext('stop', conf.stop, stop);
171 | }
172 |
173 | if (conf.hasOwnProperty('refresh')) {
174 | if (typeof (conf.refresh) === 'string')
175 | refresh.exec = conf.refresh;
176 | else
177 | checkAndApplyContext('refresh', conf.refresh, refresh);
178 | }
179 |
180 | if (conf.hasOwnProperty('dependencies')) {
181 | assert.arrayOfString(conf.dependencies, 'dependencies');
182 | conf.dependencies.forEach(function (dep) {
183 | deps.push(dep);
184 | });
185 | }
186 |
187 | assert.optionalString(conf.category, 'category');
188 | fmri = util.format('%s/%s',
189 | (conf.category || 'application'), conf.ident);
190 |
191 | xml.emitDoctype('service_bundle', 'SYSTEM',
192 | '/usr/share/lib/xml/dtd/service_bundle.dtd.1');
193 | xml.emitComment(HEADER_COMMENT);
194 |
195 | xml.emitStart('service_bundle', {
196 | 'type': 'manifest',
197 | /* JSSTYLED */
198 | 'name': fmri.replace(/\//g, '-')
199 | });
200 |
201 | xml.emitStart('service', {
202 | 'name': fmri,
203 | 'type': 'service',
204 | 'version': '1'
205 | });
206 |
207 | var enabled = conf.hasOwnProperty('enabled') ? conf.enabled : true;
208 | xml.emitEmpty('create_default_instance', { 'enabled': enabled });
209 |
210 | i = 0;
211 | deps.forEach(function (dep) {
212 | xml.emitStart('dependency', {
213 | 'name': 'dep' + i++,
214 | 'grouping': 'require_all',
215 | 'restart_on': 'error',
216 | 'type': 'service'
217 | });
218 |
219 | xml.emitEmpty('service_fmri', { 'value': dep });
220 | xml.emitEnd('dependency');
221 | });
222 |
223 | emitMethod(xml, start);
224 | emitMethod(xml, stop);
225 | if (conf.hasOwnProperty('refresh'))
226 | emitMethod(xml, refresh);
227 |
228 | xml.emitStart('template');
229 | xml.emitStart('common_name');
230 | xml.emitStart('loctext', {
231 | 'xml:lang': 'C'
232 | }, { 'bare': true });
233 | xml.emitCData(conf['label']);
234 | xml.emitEnd('loctext', { 'bare': true });
235 | xml.emitEnd('common_name');
236 | xml.emitEnd('template');
237 |
238 | xml.emitEnd('service');
239 | xml.emitEnd('service_bundle');
240 | }
241 |
242 | /*
243 | * Basic interface for emitting well-formed XML. This isn't bulletproof, but it
244 | * does escape values (not tags or keys) and checks for basic errors.
245 | */
246 | function XmlEmitter(stream)
247 | {
248 | this.xe_stream = stream;
249 | this.xe_stack = [];
250 | }
251 |
252 | XmlEmitter.prototype.emitDoctype = function (name, type, path)
253 | {
254 | this.xe_stream.write('\n');
255 | this.xe_stream.write('\n');
257 | };
258 |
259 | XmlEmitter.prototype.escape = function (str)
260 | {
261 | /* BEGIN JSSTYLED */
262 | return (str.toString().replace(/&/g, '&').
263 | replace(//g, '>').
265 | replace(/"/g, '"'));
266 | /* END JSSTYLED */
267 | };
268 |
269 | XmlEmitter.prototype.emitIndent = function ()
270 | {
271 | var str = '';
272 | var i;
273 |
274 | for (i = 0; i < this.xe_stack.length; i++)
275 | str += ' ';
276 |
277 | this.xe_stream.write(str);
278 | };
279 |
280 | XmlEmitter.prototype.emitEmpty = function (name, attrs)
281 | {
282 | this.emitIndent();
283 | this.xe_stream.write('<' + name + ' ');
284 | this.emitAttrs(attrs);
285 | this.xe_stream.write('/>\n');
286 | };
287 |
288 | XmlEmitter.prototype.emitAttrs = function (attrs)
289 | {
290 | var key;
291 |
292 | if (!attrs)
293 | return;
294 |
295 | for (key in attrs)
296 | this.xe_stream.write(key + '=\"' +
297 | this.escape(attrs[key]) + '\" ');
298 | };
299 |
300 | XmlEmitter.prototype.emitStart = function (name, attrs, opts)
301 | {
302 | this.emitIndent();
303 | this.xe_stack.push(name);
304 |
305 | this.xe_stream.write('<' + name + ' ');
306 | this.emitAttrs(attrs);
307 | this.xe_stream.write('>');
308 |
309 | if (!opts || !opts['bare'])
310 | this.xe_stream.write('\n');
311 | };
312 |
313 | XmlEmitter.prototype.emitEnd = function (name, opts)
314 | {
315 | var check = this.xe_stack.pop();
316 |
317 | assert.equal(name, check);
318 |
319 | if (!opts || !opts['bare'])
320 | this.emitIndent();
321 |
322 | this.xe_stream.write('' + name + '>\n');
323 | };
324 |
325 | XmlEmitter.prototype.emitCData = function (data)
326 | {
327 | this.xe_stream.write(this.escape(data));
328 | };
329 |
330 | XmlEmitter.prototype.emitComment = function (content)
331 | {
332 | this.xe_stream.write('\n');
333 | };
334 |
335 | function usage()
336 | {
337 | var msg = [
338 | util.format('Usage: %s [options]', PROGNAME),
339 | '',
340 | 'Generate an SMF manifest from a JSON or CLI description of the service',
341 | '',
342 | 'Examples',
343 | ' Create a service from a JSON file:',
344 | ' $ cat service.json | smfgen',
345 | '',
346 | ' Create a service from CLI arguments:',
347 | ' $ smfgen -i my-service -l "My Service" -s /opt/custom/bin/my-service',
348 | '',
349 | 'Options',
350 | ' -c, --category service category, defaults to "application"',
351 | ' -d, --cwd the directory to use when executing methods',
352 | ' -D, --dependency service dependency, multiple arguments allowed',
353 | ' -e, --env a single env var to use in the form of KEY=value, multiple arguments allowed',
354 | ' -g, --group group to use when executing methods',
355 | ' -h, --help print this usage message and exit',
356 | ' -i, --ident SMF identifier basename',
357 | ' -l, --label