forked from TritonDataCenter/moray
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pg-setup.js
420 lines (353 loc) · 11.9 KB
/
pg-setup.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
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
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
* Copyright (c) 2018, Joyent, Inc.
*/
var assert = require('assert-plus');
var exeunt = require('exeunt');
var libuuid = require('libuuid');
var mod_artedi = require('artedi');
var mod_cmd = require('./lib/cmd');
var mod_forkexec = require('forkexec');
var mod_fs = require('fs');
var mod_getopt = require('posix-getopt');
var mod_jsprim = require('jsprim');
var mod_manatee = require('node-manatee');
var mod_pg = require('./lib/pg');
var mod_schema = require('./lib/schema');
var vasync = require('vasync');
var VError = require('verror');
// --- Globals
var FLAVOR = 'sdc';
var DBNAME = process.env.MORAY_DB_NAME || 'moray';
var MORAY_USER = 'moray';
var RESERVE_CONNS = 18;
var SMF_EXIT_NODAEMON = 94;
var SUCCESSFUL_SETUP_SENTINEL = '/var/tmp/.moray-pg-setup-done';
var CREATE_BUCKETS_CFG_SQL = 'CREATE TABLE IF NOT EXISTS buckets_config (' +
'name text PRIMARY KEY, ' +
'index text NOT NULL, ' +
'pre text NOT NULL, ' +
'post text NOT NULL, ' +
'options text, ' +
'mtime timestamp without time zone DEFAULT now() NOT NULL' +
');';
var ALTER_BUCKETS_CFG_SQL =
'ALTER TABLE buckets_config OWNER TO ' + MORAY_USER + ';';
var NAME = 'pg-setup';
var LOG = mod_cmd.setupLogger(NAME);
// --- Helpers
function query(opts, sql, args, callback) {
opts.db.pg(function (cErr, pg) {
if (cErr) {
callback(new VError(cErr,
'failed to acquire client for (sql=%j)', sql));
return;
}
var reqid = libuuid.create();
pg.setRequestId(reqid);
var req = pg.query(sql, args);
var results = [];
LOG.info({
req_id: reqid,
sql: sql,
args: args
}, 'running postgres query');
req.on('error', function (qErr) {
pg.release();
callback(qErr);
});
req.on('row', function (r) {
results.push(r);
});
req.on('end', function (_) {
pg.release();
callback(null, results);
});
});
}
// --- Postgres Setup
/**
* Repeat a simple query until we get a result back, to confirm that the
* database is now ready.
*/
function waitUntilReady(opts, callback) {
var args = [
'psql',
'-U', 'postgres',
'-d', 'postgres',
'-h', opts.primary.address,
'-p', opts.primary.port.toString(),
'-c', 'SELECT now() AS when;'
];
LOG.info({ cmd: 'psql', argv: args }, 'Executing command');
mod_forkexec.forkExecWait({ argv: args },
function (err, info) {
if (err) {
LOG.warn(info, 'database not yet ready');
setImmediate(waitUntilReady, opts, callback);
return;
}
LOG.info({ info: info }, 'database is now ready');
callback();
});
}
/**
* We create a non-superuser account for Moray to use, not just to help with
* locking down its capabilities but also so that the reserved connections are
* actually useful. The user will be able to create new tables, but not new
* roles.
*
* If the user already exists, then the command will fail. The Postgres CLI
* tools don't do a great job at communicating the reason for failure in their
* return codes, so, to keep this script idempotent, we ignore the failure and
* just error out later on when running SQL that expects the user to exist.
*/
function createUser(opts, callback) {
var args = [
'createuser',
'-U', 'postgres',
'-h', opts.primary.address,
'-p', opts.primary.port.toString(),
'-d', '-S', '-R',
MORAY_USER
];
LOG.info({ cmd: 'createuser', argv: args }, 'Executing command');
mod_forkexec.forkExecWait({ argv: args },
function (err, info) {
if (err) {
LOG.warn(info,
'failed to create %j user; ' +
'continuing under the assumption that it already exists',
MORAY_USER);
} else {
LOG.info('created new %s user', MORAY_USER);
}
/*
* We may have already created the user, so we don't propagate the
* error.
*/
callback();
});
}
/**
* Having 18 reserve connections ensures that the maximum possible number of
* Moray postgres connections does not exceed the imposed "moray"
* rolconnlimit in any of the default deployment sizes: coal, lab,
* production.
*
* pg_max_conns procs_per_zone num_zones max_conns_per_proc
* coal 100 1 1 16
* lab 210 4 3 16
* prod 1000 4 3 16
*
* pg_max_conns - the default value of the postgres parameter
* max_connections set in postgres.conf for each deployment size.
*
* procs_per_zone - the default number of processes per Moray zone for the
* given deployment size.
*
* num_zones - the default number of Moray zones per shard for the
* deployment size.
*
* max_conns_per_proc - the default value of the SAPI tunable
* MORAY_MAX_PG_CONNS.
*
* Reserving 18 connections imposes an upper bound of 82, 192, and 982 moray
* role connections in coal, lab, and production deployments. These upper
* bounds are fine because with their default configurations, coal, lab, and
* production deployment Morays may have (in aggregate) a maximum of 16,
* 192, and 192 total connections to postgres, respectively.
*/
function setConnLimit(opts, callback) {
if (FLAVOR !== 'manta') {
callback();
return;
}
query(opts, 'SHOW max_connections;', [], function (sErr, results) {
if (sErr) {
LOG.warn(sErr, 'Unable to retrieve postgres max_connections; ' +
'role property \'rolconnlimit\' not applied to \'moray\'');
callback();
return;
}
if (results.length < 1) {
LOG.warn('no "max_connections" results returned; ' +
'role property \'rolconnlimit\' not applied to \'moray\'');
callback();
return;
}
var maxconns = results[0].max_connections;
if (maxconns <= RESERVE_CONNS) {
LOG.warn({
max_connections: maxconns,
reserve_connections: RESERVE_CONNS
}, 'Maximum allowed Postgres connections is lower than the ' +
'number of reserve connections; ' +
'role property \'rolconnlimit\' not applied to \'moray\'');
callback();
return;
}
var limit = maxconns - RESERVE_CONNS;
var sql = 'ALTER ROLE ' +
MORAY_USER + ' WITH CONNECTION LIMIT ' + limit;
query(opts, sql, [], function (aErr) {
if (aErr) {
callback(new VError(aErr, 'failed to set connection limit'));
return;
}
LOG.info('Successfully set %s user connection limit to %d',
MORAY_USER, limit);
callback();
});
});
}
/**
* Setup the database for Moray to use. Since we can't conditionally create the
* database like we can tables, we ignore any errors here, since it likely
* already exists.
*/
function createDB(opts, callback) {
var args = [
'createdb',
'-U', 'postgres',
'-T', 'template0',
'--locale=C',
'-E', 'UNICODE',
'-O', MORAY_USER,
'-h', opts.primary.address,
'-p', opts.primary.port.toString(),
DBNAME
];
LOG.info({ cmd: 'createdb', argv: args }, 'Executing command');
mod_forkexec.forkExecWait({ argv: args }, function (err, info) {
if (err) {
LOG.warn(info, 'failed to create %j database', DBNAME);
} else {
LOG.info(info, 'created new %j database', DBNAME);
}
/*
* We may have already created the database, so we don't propagate the
* error.
*/
callback();
});
}
function createTable(opts, callback) {
query(opts, CREATE_BUCKETS_CFG_SQL, [], callback);
}
function changeTableOwner(opts, callback) {
query(opts, ALTER_BUCKETS_CFG_SQL, [], callback);
}
/**
* While this script is designed to be idempotent, there's really no need for
* us to always try it. Once we've done everything, we write out a file so that
* we won't retry on future reboots. (Reprovisions will wipe the file though,
* and we'll run again then.)
*/
function writeSentinel(_, callback) {
mod_fs.writeFile(SUCCESSFUL_SETUP_SENTINEL, '', callback);
}
function setupPostgres(opts, callback) {
if (mod_fs.existsSync(SUCCESSFUL_SETUP_SENTINEL)) {
LOG.info('Found %j, skipping setup', SUCCESSFUL_SETUP_SENTINEL);
setImmediate(callback);
return;
}
vasync.pipeline({
arg: opts,
funcs: [
waitUntilReady,
createUser,
setConnLimit,
createDB,
createTable,
changeTableOwner,
writeSentinel
]
}, callback);
}
function parseOptions() {
var parser = new mod_getopt.BasicParser(':vf:r:', process.argv);
var option;
var opts = {};
while ((option = parser.getopt()) !== undefined) {
switch (option.option) {
case 'f':
opts.file = option.optarg;
break;
case 'v':
LOG = mod_cmd.increaseVerbosity(LOG);
break;
case 'r':
FLAVOR = option.optarg;
break;
case ':':
throw new VError('Expected argument for -%s', option.optopt);
default:
throw new VError('Invalid option: -%s', option.optopt);
}
}
if (parser.optind() !== process.argv.length) {
throw new VError(
'Positional arguments found when none were expected: %s',
process.argv.slice(parser.optind()).join(' '));
}
if (!opts.file) {
LOG.fatal({ opts: opts }, 'No config file specified.');
throw new Error('No config file specified');
}
return (opts);
}
function main() {
var options = parseOptions();
var config = mod_cmd.readConfig(options);
config.log = LOG;
mod_schema.validateConfig(config);
assert.object(config.manatee, 'config.manatee');
var dbopts = config.manatee;
dbopts.manatee.log = LOG;
var dbresolver = mod_manatee.createPrimaryResolver(dbopts.manatee);
LOG.info('Fetching Manatee state');
/*
* We wait indefinitely until the Manatee cluster is ready and we have a
* primary that we can connect to. If there are multiple Moray instances,
* they'll all start trying to initialize the database at once. Since all
* of the operations here are okay to rerun, this should be okay.
*
* If the primary changes underneath us then the CLI tools will fail, and
* we'll exit this script. SMF will then take care of restarting us, and
* we'll try again with the new primary. (If this happens enough times,
* then we'll go into maintenance, and an operator will have to intervene.)
*/
dbresolver.once('added', function (_, primary) {
var collector = mod_artedi.createCollector({ labels: {} });
var db = mod_pg.createPool(mod_jsprim.mergeObjects(dbopts.pg, {
log: LOG,
domain: 'manatee',
user: 'postgres',
collector: collector,
resolver: dbresolver
}));
setupPostgres({
resolver: dbresolver,
primary: primary,
config: config,
db: db
}, function (err) {
db.close();
if (err) {
LOG.error(err, 'failed to set up postgres');
exeunt(1);
} else {
LOG.info('successfully set up postgres');
exeunt(SMF_EXIT_NODAEMON);
}
});
});
dbresolver.start();
}
main();