diff --git a/packages/system/lib/cgroups.js b/packages/system/lib/cgroups.js deleted file mode 100644 index cf47e7197..000000000 --- a/packages/system/lib/cgroups.js +++ /dev/null @@ -1,340 +0,0 @@ -// Generated by CoffeeScript 2.7.0 - // # `nikita.system.cgroups` - -// Nikita action to manipulate cgroups. [cgconfig.conf(5)] describes the - // configuration file used by libcgroup to define control groups, their parameters - // and also mount points. The configuration file is identical on Ubuntu, RedHat - // and CentOS. - -// ## Implementation - -// When reading the current config, nikita uses `cgsnapshot` command in order to - // have a well formatted file. It is available on CentOS with the `libcgroup-tools` - // package. - -// If docker is installed and started, informations about live containers could be - // printed, that's why all path under docker/* are ignored. - -// ## Example - -// Example of a group object: - -// ``` - // bibi: - // perm: - // admin: - // uid: 'bibi' - // gid: 'bibi' - // task: - // uid: 'bibi' - // gid: 'bibi' - // controllers: - // cpu: - // 'cpu.rt_period_us': '"1000000"' - // 'cpu.rt_runtime_us': '"0"' - // 'cpu.cfs_period_us': '"100000"' - // ``` - -// Which will result in a file: - -// ```text - // group bibi { - // perm { - // admin { - // uid = bibi; - // gid = bibi; - // } - // task { - // uid = bibi; - // gid = bibi; - // } - // } - // cpu { - // cpu.rt_period_us = "1000000"; - // cpu.rt_runtime_us = "0"; - // cpu.cfs_period_us = "100000"; - // } - // } - // ``` - -// ## Schema definitions -var definitions, handler, merge, path, utils, - indexOf = [].indexOf; - -definitions = { - config: { - type: 'object', - properties: { - 'default': { - $ref: '#/definitions/group', - description: `The default object of cgconfig file.` - }, - 'groups': { - type: 'object', - description: `Object of cgroups to add to cgconfig file. The keys are the -cgroup name, and the values are the cgroup configuration.`, - patternProperties: { - '.*': { // cgroup name - $ref: '#/definitions/group' - } - }, - additionalProperties: false - }, - 'ignore': { - type: 'array', - items: { - type: 'string' - }, - description: `List of group path to ignore. Only used when merging.` - }, - 'mounts': { - type: 'array', - description: `List of mount object to add to cgconfig file.` - }, - 'merge': { - type: 'boolean', - default: true, - description: `Default to true. Read the config from cgsnapshot command and merge -mounts part of the cgroups.` - }, - 'target': { - type: 'string', - description: `The cgconfig configuration file. By default nikita detects provider -based on os.` - } - }, - anyOf: [ - { - required: ['groups'] - }, - { - required: ['mounts'] - }, - { - required: ['default'] - } - ] - }, - group: { - type: 'object', - description: `Controllers in the cgroup where the keys represent the name of the -controler.`, - properties: { - perm: { - type: 'object', - description: `Object to describe the taks and limits permissions.`, - properties: { - 'admin': { - $ref: '#/definitions/group_perm', - description: `Who can manage limits` - }, - 'task': { - $ref: '#/definitions/group_perm', - description: `Who can add tasks to this group` - } - } - }, - cpuset: { - $ref: '#/definitions/group_controller' - }, - cpu: { - $ref: '#/definitions/group_controller' - }, - cpuacct: { - $ref: '#/definitions/group_controller' - }, - blkio: { - $ref: '#/definitions/group_controller' - }, - memory: { - $ref: '#/definitions/group_controller' - }, - devices: { - $ref: '#/definitions/group_controller' - }, - freezer: { - $ref: '#/definitions/group_controller' - }, - net_cls: { - $ref: '#/definitions/group_controller' - }, - perf_event: { - $ref: '#/definitions/group_controller' - }, - net_prio: { - $ref: '#/definitions/group_controller' - }, - hugetlb: { - $ref: '#/definitions/group_controller' - }, - pids: { - $ref: '#/definitions/group_controller' - }, - rdma: { - $ref: '#/definitions/group_controller' - } - } - }, - group_perm: { - type: 'object', - properties: { - 'uid': { - oneOf: [ - { - type: 'integer' - }, - { - type: 'string' - } - ] - }, - 'gid': { - oneOf: [ - { - type: 'integer' - }, - { - type: 'string' - } - ] - } - } - }, - group_controller: { - type: 'object', - patternProperties: { - '.*': { - oneOf: [ - { - type: 'integer' - }, - { - type: 'string' - } - ] - } - }, - additionalProperties: false - } -}; - -// ## Handler -handler = async function({config}) { - var cgconfig, cpuaccts, cpus, group, groups, majorVersion, name, os, ref, stdout, store; - // throw Error 'Missing cgroups content' unless config.groups? or config.mounts? or config.default? - if (config.mounts == null) { - config.mounts = []; - } - if (config.groups == null) { - config.groups = {}; - } - if (config.merge == null) { - config.merge = true; - } - config.cgconfig = {}; - config.cgconfig['mounts'] = config.mounts; - config.cgconfig['groups'] = config.groups; - if (config.default != null) { - config.cgconfig['groups'][''] = config.default; - } - if (config.ignore == null) { - config.ignore = []; - } - if (!Array.isArray(config.ignore)) { - config.ignore = [config.ignore]; - } - // Detect Os and version - ({os} = (await this.system.info.os())); - // configure parameters based on previous OS dection - store = {}; - // Enable cgroup for all distribution, it was restricted to rhel systems - // if ['redhat','centos'].includes os.distribution - if (true) { - ({stdout} = (await this.execute({ - $shy: true, - command: 'cgsnapshot -s 2>&1' - }))); - cgconfig = utils.cgconfig.parse(stdout); - if (cgconfig.mounts == null) { - cgconfig.mounts = []; - } - cpus = cgconfig.mounts.filter(function(mount) { - return mount.type === 'cpu'; - }); - cpuaccts = cgconfig.mounts.filter(function(mount) { - return mount.type === 'cpuacct'; - }); - // We choose a path which is mounted by default - // if not @store['nikita:cgroups:cpu_path']? - if (cpus.length > 0) { - store.cpu_path = cpus[0]['path'].split(',')[0]; - } else { - // @store['nikita:cgroups:cpu_path'] ?= cpu_path - // a arbitrary path is given based on the - switch (os.distribution) { - case 'redhat': - case 'centos': - majorVersion = os.version.split('.')[0]; - switch (majorVersion) { - case '6': - store.cpu_path = '/cgroups/cpu'; - break; - case '7': - store.cpu_path = '/sys/fs/cgroup/cpu'; - break; - default: - throw Error("Nikita does not support cgroups for your RedHat or CentOS version}"); - } - break; - default: - throw Error(`Nikita does not support cgroups on your OS ${os.distribution}`); - } - } - store.mount = `${path.posix.dirname(store.cpu_path)}`; - // Running docker containers are remove from cgsnapshot output - if (config.merge) { - groups = {}; - ref = cgconfig.groups; - for (name in ref) { - group = ref[name]; - if (!((name.indexOf('docker/') !== -1) || (indexOf.call(config.ignore, name) >= 0))) { - groups[name] = group; - } - } - config.cgconfig.groups = merge(groups, config.groups); - config.cgconfig.mounts.push(...cgconfig.mounts); - } - } - if (['redhat', 'centos'].includes(os.distribution)) { - // Write the configuration - if (config.target == null) { - config.target = '/etc/cgconfig.conf'; - } - } - this.file(config, { - content: utils.cgconfig.stringify(config.cgconfig) - }); - return { - cgroups: { - cpu_path: store.cpu_path, - mount: store.mount - } - }; -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - definitions: definitions - } -}; - -// ## Dependencies -utils = require('./utils'); - -({merge} = require('mixme')); - -path = require('path'); - -// [cgconfig.conf(5)]: https://linux.die.net/man/5/cgconfig.conf diff --git a/packages/system/lib/cgroups/README.md b/packages/system/lib/cgroups/README.md new file mode 100644 index 000000000..3f162ef7e --- /dev/null +++ b/packages/system/lib/cgroups/README.md @@ -0,0 +1,58 @@ + +# `nikita.system.cgroups` + +Nikita action to manipulate cgroups. [cgconfig.conf(5)] describes the +configuration file used by libcgroup to define control groups, their parameters +and also mount points. The configuration file is identical on Ubuntu, RedHat +and CentOS. + +## Implementation + +When reading the current config, nikita uses `cgsnapshot` command in order to +have a well formatted file. It is available on CentOS with the `libcgroup-tools` +package. + +If docker is installed and started, informations about live containers could be +printed, that's why all path under docker/* are ignored. + +## Example + +Example of a group object: + +``` +bibi: + perm: + admin: + uid: 'bibi' + gid: 'bibi' + task: + uid: 'bibi' + gid: 'bibi' + controllers: + cpu: + 'cpu.rt_period_us': '"1000000"' + 'cpu.rt_runtime_us': '"0"' + 'cpu.cfs_period_us': '"100000"' +``` + +Which will result in a file: + +```text +group bibi { + perm { + admin { + uid = bibi; + gid = bibi; + } + task { + uid = bibi; + gid = bibi; + } + } + cpu { + cpu.rt_period_us = "1000000"; + cpu.rt_runtime_us = "0"; + cpu.cfs_period_us = "100000"; + } +} +``` diff --git a/packages/system/lib/cgroups/index.js b/packages/system/lib/cgroups/index.js new file mode 100644 index 000000000..562bf5ae5 --- /dev/null +++ b/packages/system/lib/cgroups/index.js @@ -0,0 +1,105 @@ + +// Dependencies +const path = require('path'); +const {merge} = require('mixme'); +const utils = require('../utils'); +const definitions = require('./schema.json'); + +// Action +module.exports = { + handler: async function({config}) { + config.mounts ??= [] + config.groups ??= {}; + config.merge ??= true; + config.cgconfig = {}; + config.cgconfig['mounts'] = config.mounts; + config.cgconfig['groups'] = config.groups; + if (config.default != null) { + config.cgconfig['groups'][''] = config.default; + } + if (config.ignore == null) { + config.ignore = []; + } + if (!Array.isArray(config.ignore)) { + config.ignore = [config.ignore]; + } + // Detect Os and version + const {os} = (await this.system.info.os()); + // configure parameters based on previous OS dection + const store = {}; + // Enable cgroup for all distribution, it was restricted to rhel systems + // if ['redhat','centos'].includes os.distribution + if (true) { + const {stdout} = await this.execute({ + $shy: true, + command: 'cgsnapshot -s 2>&1' + }); + const cgconfig = utils.cgconfig.parse(stdout); + if (cgconfig.mounts == null) { + cgconfig.mounts = []; + } + const cpus = cgconfig.mounts.filter(function(mount) { + return mount.type === 'cpu'; + }); + const cpuaccts = cgconfig.mounts.filter(function(mount) { + return mount.type === 'cpuacct'; + }); + // We choose a path which is mounted by default + // if not @store['nikita:cgroups:cpu_path']? + if (cpus.length > 0) { + store.cpu_path = cpus[0]['path'].split(',')[0]; + } else { + // @store['nikita:cgroups:cpu_path'] ?= cpu_path + // a arbitrary path is given based on the + switch (os.distribution) { + case 'redhat': + case 'centos': + const majorVersion = os.version.split('.')[0]; + switch (majorVersion) { + case '6': + store.cpu_path = '/cgroups/cpu'; + break; + case '7': + store.cpu_path = '/sys/fs/cgroup/cpu'; + break; + default: + throw Error("Nikita does not support cgroups for your RedHat or CentOS version}"); + } + break; + default: + throw Error(`Nikita does not support cgroups on your OS ${os.distribution}`); + } + } + store.mount = `${path.posix.dirname(store.cpu_path)}`; + // Running docker containers are remove from cgsnapshot output + if (config.merge) { + const groups = {}; + for (const name in cgconfig.groups) { + if (!(name.includes('docker/') || config.ignore.includes(name))) { + groups[name] = cgconfig.groups[name]; + } + } + config.cgconfig.groups = merge(groups, config.groups); + config.cgconfig.mounts.push(...cgconfig.mounts); + } + } + if (['redhat', 'centos'].includes(os.distribution)) { + // Write the configuration + if (config.target == null) { + config.target = '/etc/cgconfig.conf'; + } + } + this.file(config, { + content: utils.cgconfig.stringify(config.cgconfig) + }); + return { + cgroups: { + cpu_path: store.cpu_path, + mount: store.mount + } + }; + }, + metadata: { + definitions: definitions + } +}; diff --git a/packages/system/lib/cgroups/schema.json b/packages/system/lib/cgroups/schema.json new file mode 100644 index 000000000..e3a224080 --- /dev/null +++ b/packages/system/lib/cgroups/schema.json @@ -0,0 +1,158 @@ +{ + "config": { + "type": "object", + "properties": { + "default": { + "$ref": "#/definitions/group", + "description": "The default object of cgconfig file." + }, + "groups": { + "type": "object", + "description": "Object of cgroups to add to cgconfig file. The keys are the\ncgroup name, and the values are the cgroup configuration.", + "patternProperties": { + ".*": { + "$ref": "#/definitions/group" + } + }, + "additionalProperties": false + }, + "ignore": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of group path to ignore. Only used when merging." + }, + "mounts": { + "type": "array", + "description": "List of mount object to add to cgconfig file." + }, + "merge": { + "type": "boolean", + "default": true, + "description": "Default to true. Read the config from cgsnapshot command and merge\nmounts part of the cgroups." + }, + "target": { + "type": "string", + "description": "The cgconfig configuration file. By default nikita detects provider\nbased on os." + } + }, + "anyOf": [ + { + "required": [ + "groups" + ] + }, + { + "required": [ + "mounts" + ] + }, + { + "required": [ + "default" + ] + } + ] + }, + "group": { + "type": "object", + "description": "Controllers in the cgroup where the keys represent the name of the\ncontroler.", + "properties": { + "perm": { + "type": "object", + "description": "Object to describe the taks and limits permissions.", + "properties": { + "admin": { + "$ref": "#/definitions/group_perm", + "description": "Who can manage limits" + }, + "task": { + "$ref": "#/definitions/group_perm", + "description": "Who can add tasks to this group" + } + } + }, + "cpuset": { + "$ref": "#/definitions/group_controller" + }, + "cpu": { + "$ref": "#/definitions/group_controller" + }, + "cpuacct": { + "$ref": "#/definitions/group_controller" + }, + "blkio": { + "$ref": "#/definitions/group_controller" + }, + "memory": { + "$ref": "#/definitions/group_controller" + }, + "devices": { + "$ref": "#/definitions/group_controller" + }, + "freezer": { + "$ref": "#/definitions/group_controller" + }, + "net_cls": { + "$ref": "#/definitions/group_controller" + }, + "perf_event": { + "$ref": "#/definitions/group_controller" + }, + "net_prio": { + "$ref": "#/definitions/group_controller" + }, + "hugetlb": { + "$ref": "#/definitions/group_controller" + }, + "pids": { + "$ref": "#/definitions/group_controller" + }, + "rdma": { + "$ref": "#/definitions/group_controller" + } + } + }, + "group_perm": { + "type": "object", + "properties": { + "uid": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "gid": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + } + }, + "group_controller": { + "type": "object", + "patternProperties": { + ".*": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + } + }, + "additionalProperties": false + } +} diff --git a/packages/system/lib/group/README.md b/packages/system/lib/group/README.md new file mode 100644 index 000000000..bf9bf1dff --- /dev/null +++ b/packages/system/lib/group/README.md @@ -0,0 +1,24 @@ + +# `nikita.system.group` + +Create or modify a Unix group. + +## Callback Parameters + +* `$status` + Value is "true" if group was created or modified. + +## Example + +```js +const {$status} = await nikita.system.group({ + name: 'myself' + system: true + gid: 490 +}); +console.log(`Group was created/modified: ${$status}`); +``` + +The result of the above action can be viewed with the command +`cat /etc/group | grep myself` producing an output similar to +"myself:x:490:". diff --git a/packages/system/lib/group/index.js b/packages/system/lib/group/index.js index 020f00859..c861ba470 100644 --- a/packages/system/lib/group/index.js +++ b/packages/system/lib/group/index.js @@ -1,125 +1,72 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.group` -// Create or modify a Unix group. +// Dependencies +const definitions = require('./schema.json'); +const utils = require('../utils'); +const esa = utils.string.escapeshellarg; -// ## Callback Parameters - -// * `$status` -// Value is "true" if group was created or modified. - -// ## Example - -// ```js -// const {$status} = await nikita.system.group({ -// name: 'myself' -// system: true -// gid: 490 -// }); -// console.log(`Group was created/modified: ${$status}`); -// ``` - -// The result of the above action can be viewed with the command -// `cat /etc/group | grep myself` producing an output similar to -// "myself:x:490:". - -// ## Hooks -var definitions, handler, on_action; - -on_action = function({config}) { - if (typeof config.gid === 'string') { - return config.gid = parseInt(config.gid, 10); - } -}; - -// ## Schema definitions -definitions = { - config: { - type: 'object', - properties: { - 'gid': { - type: 'integer', - description: `Group name or number of the user´s initial login group.` - }, - 'name': { - type: 'string', - description: `Login name of the group.` - }, - 'system': { - type: 'boolean', - default: false, - description: `Create a system account, such user are not created with ahome by -default, set the "home" option if we it to be created.` - } - }, - required: ['name'] - } -}; - -// ## Handler -handler = async function({ - config, - tools: {log} - }) { - var $status, changes, groups, info; - if (config.system == null) { - config.system = false; - } - if (config.gid == null) { - config.gid = null; - } - // throw Error 'Invalid gid option' if config.gid? and isNaN config.gid - ({groups} = (await this.system.group.read())); - info = groups[config.name]; - log(info ? { - message: `Got group information for ${JSON.stringify(config.name)}`, - level: 'DEBUG', - module: 'nikita/lib/system/group' - } : { - message: `Group ${JSON.stringify(config.name)} not present`, - level: 'DEBUG', - module: 'nikita/lib/system/group' - }); - if (!info) { // Create group - ({$status} = (await this.execute({ - command: ['groupadd', config.system ? '-r' : void 0, config.gid != null ? `-g ${config.gid}` : void 0, config.name].join(' '), - code: [0, 9] - }))); - if (!$status) { // Modify group - return log({ - message: "Group defined elsewhere than '/etc/group', exit code is 9", - level: 'WARN' - }); - } - } else { - changes = ['gid'].filter(function(k) { - return (config[k] != null) && `${info[k]}` !== `${config[k]}`; - }); - if (changes.length) { - await this.execute({ - command: ['groupmod', config.gid ? ` -g ${config.gid}` : void 0, config.name].join(' ') - }); - return log({ - message: "Group information modified", - level: 'WARN' +// Action +module.exports = { + handler: async function ({ config, tools: { log } }) { + // throw Error 'Invalid gid option' if config.gid? and isNaN config.gid + const { groups } = await this.system.group.read(); + const info = groups[config.name]; + log( + "DEBUG", + info + ? `Got group information for ${JSON.stringify(config.name)}` + : `Group ${JSON.stringify(config.name)} not present` + ); + if (!info) { + // Create group + const { $status } = await this.execute({ + command: [ + "groupadd", + config.system && "-r", + config.gid != null && `-g ${esa(''+config.gid)}`, + esa(config.name), + ].filter(Boolean).join(" "), + code: [0, 9], }); + if (!$status) { + // Modify group + log({ + message: "Group defined elsewhere than '/etc/group', exit code is 9", + level: "WARN", + }); + } } else { - return log({ - message: "Group information unchanged", - level: 'INFO' - }); + const changes = ["gid"].filter( (k) => + config[k] != null && `${info[k]}` !== `${config[k]}` + ); + if (changes.length) { + await this.execute({ + command: [ + "groupmod", + config.gid && ` -g ${esa(config.gid)}`, + esa(config.name), + ].join(" "), + }); + log({ + message: "Group information modified", + level: "WARN", + }); + } else { + log({ + message: "Group information unchanged", + level: "INFO", + }); + } } - } -}; - -// ## Exports -module.exports = { - handler: handler, + }, hooks: { - on_action: on_action + on_action: function ({ config }) { + if (typeof config.gid === "string") { + config.gid = parseInt(config.gid, 10); + } + }, }, metadata: { - argument_to_config: 'name', - definitions: definitions - } + argument_to_config: "name", + definitions: definitions, + }, }; diff --git a/packages/system/lib/group/read.js b/packages/system/lib/group/read.js deleted file mode 100644 index 376d235a3..000000000 --- a/packages/system/lib/group/read.js +++ /dev/null @@ -1,144 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.group.read` - -// Read and parse the group definition file located in "/etc/group". - -// ## Output parameters - -// * `groups` -// An object where keys are the group names and values are the groups properties. -// See the parameter `group` for a list of available properties. -// * `group` -// Properties associated witht the group, only if the input parameter `gid` is -// provided. Available properties are: -// * `group` (string) -// Name of the group. -// * `password` (string) -// Group password as a result of the `crypt` function, rarely used. -// * `gid` (string) -// The numerical equivalent of the group name. It is used by the operating -// system and applications when determining access privileges. -// * `users` (array[string]) -// List of users who are members of this group. - -// ## Examples - -// Retrieve all groups informations: - -// ```js -// const {groups} = await nikita.system.group.read() -// console.info("Available groups:", groups) -// ``` - -// Retrieve information of an individual group: - -// ```js -// const {group} = await nikita.system.group.read({ -// gid: 1 -// }) -// console.info("The group found:", group) -// ``` - -// ## Schema definitions -var definitions, handler, utils; - -definitions = { - config: { - type: 'object', - properties: { - 'gid': { - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid', - description: `Retrieve the information for a specific group name or gid.` - }, - 'target': { - type: 'string', - default: '/etc/group', - description: `Path to the group definition file, default to "/etc/group".` - } - } - } -}; - -// ## Handler -handler = async function({ - config, - metadata, - state, - tools: {log} - }) { - var data, group, groups, stdout, str2groups; - if (typeof config.gid === 'string' && /\d+/.test(config.gid)) { - config.gid = parseInt(config.gid, 10); - } - // Parse the groups output - str2groups = function(data) { - var groups, i, len, line, ref; - groups = {}; - ref = utils.string.lines(data); - for (i = 0, len = ref.length; i < len; i++) { - line = ref[i]; - line = /(.*)\:(.*)\:(.*)\:(.*)/.exec(line); - if (!line) { - continue; - } - groups[line[1]] = { - group: line[1], - password: line[2], - gid: parseInt(line[3]), - users: line[4] ? line[4].split(',') : [] - }; - } - return groups; - }; - // Fetch the groups information - if (!config.target) { - ({stdout} = (await this.execute({ - command: 'getent group' - }))); - groups = str2groups(stdout); - } else { - ({data} = (await this.fs.base.readFile({ - target: config.target, - encoding: 'ascii' - }))); - groups = str2groups(data); - } - if (!config.gid) { - return { - // Return all the groups - groups: groups - }; - } - // Return a group by name - if (typeof config.gid === 'string') { - group = groups[config.gid]; - if (!group) { - throw Error(`Invalid Option: no gid matching ${JSON.stringify(config.gid)}`); - } - return { - group: group - }; - } else { - // Return a group by gid - group = Object.values(groups).filter(function(group) { - return group.gid === config.gid; - })[0]; - if (!group) { - throw Error(`Invalid Option: no gid matching ${JSON.stringify(config.gid)}`); - } - return { - group: group - }; - } -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - definitions: definitions - } -}; - -// ## Dependencies -utils = require('../utils'); diff --git a/packages/system/lib/group/read/README.md b/packages/system/lib/group/read/README.md new file mode 100644 index 000000000..4826db0b9 --- /dev/null +++ b/packages/system/lib/group/read/README.md @@ -0,0 +1,40 @@ + +# `nikita.system.group.read` + +Read and parse the group definition file located in "/etc/group". + +## Output parameters + +* `groups` + An object where keys are the group names and values are the groups properties. + See the parameter `group` for a list of available properties. +* `group` + Properties associated witht the group, only if the input parameter `gid` is + provided. Available properties are: + * `group` (string) + Name of the group. + * `password` (string) + Group password as a result of the `crypt` function, rarely used. + * `gid` (string) + The numerical equivalent of the group name. It is used by the operating + system and applications when determining access privileges. + * `users` (array[string]) + List of users who are members of this group. + +## Examples + +Retrieve all groups informations: + +```js +const {groups} = await nikita.system.group.read() +console.info("Available groups:", groups) +``` + +Retrieve information of an individual group: + +```js +const {group} = await nikita.system.group.read({ + gid: 1 +}) +console.info("The group found:", group) +``` diff --git a/packages/system/lib/group/read/index.js b/packages/system/lib/group/read/index.js new file mode 100644 index 000000000..a43b450d6 --- /dev/null +++ b/packages/system/lib/group/read/index.js @@ -0,0 +1,79 @@ + +// Dependencies +const definitions = require('./schema.json'); +const utils = require('../../utils'); + +// Parse the groups output +const str2groups = function (data) { + const groups = {}; + for (const line of utils.string.lines(data)) { + const group = /(.*)\:(.*)\:(.*)\:(.*)/.exec(line); + if (!group) { + continue; + } + groups[group[1]] = { + group: group[1], + password: group[2], + gid: parseInt(group[3]), + users: group[4] ? group[4].split(",") : [], + }; + } + return groups; +}; + +// Action +module.exports = { + handler: async function ({ config }) { + if (typeof config.gid === "string" && /\d+/.test(config.gid)) { + config.gid = parseInt(config.gid, 10); + } + // Fetch the groups information + let groups; + if (!config.target) { + const { stdout } = await this.execute({ + command: "getent group", + }); + groups = str2groups(stdout); + } else { + const { data } = await this.fs.base.readFile({ + target: config.target, + encoding: "ascii", + }); + groups = str2groups(data); + } + if (!config.gid) { + // Return all the groups + return { + groups: groups, + }; + } + // Return a group by name + if (typeof config.gid === "string") { + const group = groups[config.gid]; + if (!group) { + throw Error( + `Invalid Option: no gid matching ${JSON.stringify(config.gid)}` + ); + } + return { + group: group, + }; + } else { + // Return a group by gid + const group = Object.values(groups).find( + (group) => group.gid === config.gid + ); + if (!group) { + throw Error( + `Invalid Option: no gid matching ${JSON.stringify(config.gid)}` + ); + } + return { + group: group, + }; + } + }, + metadata: { + definitions: definitions, + }, +}; diff --git a/packages/system/lib/group/read/schema.json b/packages/system/lib/group/read/schema.json new file mode 100644 index 000000000..2cb06a51e --- /dev/null +++ b/packages/system/lib/group/read/schema.json @@ -0,0 +1,16 @@ +{ + "config": { + "type": "object", + "properties": { + "gid": { + "$ref": "module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid", + "description": "Retrieve the information for a specific group name or gid." + }, + "target": { + "type": "string", + "default": "/etc/group", + "description": "Path to the group definition file, default to \"/etc/group\"." + } + } + } +} diff --git a/packages/system/lib/group/remove.js b/packages/system/lib/group/remove.js deleted file mode 100644 index c656de3a9..000000000 --- a/packages/system/lib/group/remove.js +++ /dev/null @@ -1,58 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.group.remove` - -// Create or modify a Unix user. - -// ## Callback parameters - -// * `$status` -// Value is "true" if user was created or modified. - -// ## Example - -// ```js -// const {$status} = await nikita.group.remove({ -// name: 'a_user' -// }) -// console.info(`User is removed: ${$status}`) -// ``` - -// The result of the above action can be viewed with the command -// `cat /etc/passwd | grep myself` producing an output similar to -// "a\_user:x:490:490:A System User:/home/a\_user:/bin/bash". You can also check -// you are a member of the "wheel" group (gid of "10") with the command -// `id a\_user` producing an output similar to -// "uid=490(hive) gid=10(wheel) groups=10(wheel)". - -// ## Schema definitions -var definitions, handler; - -definitions = { - config: { - type: 'object', - properties: { - name: { - type: 'string', - description: `Name of the group to remove.` - } - }, - required: ['name'] - } -}; - -// ## Handler -handler = function({metadata, config}) { - return this.execute({ - command: `groupdel ${config.name}`, - code: [0, 6] - }); -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - argument_to_config: 'name', - definitions: definitions - } -}; diff --git a/packages/system/src/group/remove.coffee.md b/packages/system/lib/group/remove/README.md similarity index 55% rename from packages/system/src/group/remove.coffee.md rename to packages/system/lib/group/remove/README.md index c0ff2b9a0..cc05e72f6 100644 --- a/packages/system/src/group/remove.coffee.md +++ b/packages/system/lib/group/remove/README.md @@ -23,31 +23,3 @@ The result of the above action can be viewed with the command you are a member of the "wheel" group (gid of "10") with the command `id a\_user` producing an output similar to "uid=490(hive) gid=10(wheel) groups=10(wheel)". - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - name: - type: 'string' - description: ''' - Name of the group to remove. - ''' - required: ['name'] - -## Handler - - handler = ({metadata, config}) -> - @execute - command: "groupdel #{config.name}" - code: [0, 6] - -## Exports - - module.exports = - handler: handler - metadata: - argument_to_config: 'name' - definitions: definitions diff --git a/packages/system/lib/group/remove/index.js b/packages/system/lib/group/remove/index.js new file mode 100644 index 000000000..dd2897eab --- /dev/null +++ b/packages/system/lib/group/remove/index.js @@ -0,0 +1,17 @@ + +// Dependencies +const definitions = require('./schema.json'); + +// Action +module.exports = { + handler: function({metadata, config}) { + this.execute({ + command: `groupdel ${config.name}`, + code: [0, 6] + }); + }, + metadata: { + argument_to_config: 'name', + definitions: definitions + } +}; diff --git a/packages/system/lib/group/remove/schema.json b/packages/system/lib/group/remove/schema.json new file mode 100644 index 000000000..42103125f --- /dev/null +++ b/packages/system/lib/group/remove/schema.json @@ -0,0 +1,14 @@ +{ + "config": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the group to remove." + } + }, + "required": [ + "name" + ] + } +} diff --git a/packages/system/lib/group/schema.json b/packages/system/lib/group/schema.json new file mode 100644 index 000000000..871da827e --- /dev/null +++ b/packages/system/lib/group/schema.json @@ -0,0 +1,23 @@ +{ + "config": { + "type": "object", + "properties": { + "gid": { + "type": "integer", + "description": "Group name or number of the user´s initial login group." + }, + "name": { + "type": "string", + "description": "Login name of the group." + }, + "system": { + "type": "boolean", + "default": false, + "description": "Create a system account, such user are not created with ahome by\ndefault, set the \"home\" option if we it to be created." + } + }, + "required": [ + "name" + ] + } +} diff --git a/packages/system/lib/info/disks.js b/packages/system/lib/info/disks.js deleted file mode 100644 index c71d23b67..000000000 --- a/packages/system/lib/info/disks.js +++ /dev/null @@ -1,180 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.info.disks` - -// Expose disk information. Internally, it parse the result of the "df" command. -// The properties "total", "used" and "available" are expressed in bytes. - -// ## Implementation - -// The action rely on the `df` command and the presence of the `--output` option. - -// Tests are filtered with the `system_info_disks` tag. - -// ## Callback - -// The following properties are available: - -// - `filesystem` (string) -// The source of the mount point, usually a device; alias of the "source" -// property of the `df` command. -// - `total` (integer) -// Total space available in bytes; derivated from the "itotal" output property of -// the `df` command. -// - `used` (integer) -// Total space used in bytes; derivated from the "iused" output property of -// the `df` command. -// - `available` (integer) -// Total space available in bytes; derivated from the "iavail" output property of -// the `df` command. -// - `available_pourcent` (string) -// Total space available in pourcentage; alias of the "ipcent" output -// property of the `df` command. -// - `mountpoint` (string) -// The mount point location; alias of the "target" output property of the `df` -// command. - -// Additionnaly, the `df` property export the low level information obtained from -// the `df` command: - -// - `disks[].df.source` (string) -// The source of the mount point, usually a device; correspond to the -// "Filesystem" header of the `df` command. -// - `disks[].df.fstype` (string) -// File system type; correspond to the "Type" header of the `df` command. -// - `disks[].df.itotal` (integer) -// Total number of inodes, in bytes; correspond to the "Inodes" header of the -// `df` command. -// - `disks[].df.iused` (integer) -// Number of used inodes, in bytes; correspond to the "IUsed" header of the `df` -// command. -// - `disks[].df.iavail` (integer) -// Number of available inodes, in bytes; correspond to the "IFree" header of the -// `df` command. -// - `disks[].df.ipcent` (string) -// Percentage of iused divided by itotal; correspond to the "IUse%" header of the -// `df` command. -// - `disks[].df.size` (integer) -// The total space available, measured in 1kB units; correspond to the -// "1K-blocks" header of the `df` command. -// - `disks[].df.used` (integer) -// Number of used blocks; correspond to the "Used" header of the `df` command. -// - `disks[].df.avail` (integer) -// Number of available blocks; correspond to the -// "Avail" header of the `df` command. -// - `disks[].df.pcent` (float) -// Percentage of used divided by size; correspond to the -// "Use%" header of the `df` command. -// - `disks[].df.target` (string) -// The mount point; correspond to the -// "Mounted on" header of the `df` command. - -// Note that if you add The Used and Available columns you don't get the total size -// shown; this is because of blocks that are reserved for root as shown in the -// output of `dumpe2fs` as "Reserved block count:". Those blocks can only be used by -// root, the idea behind this is that if a user fills up the filesystem, critical -// stuff still works and root can fix the problem. - -// ## Example - -// ```js -// const {disks} = await nikita.system.info.disks() -// disks.forEach((disk) => { -// console.info('File system:', disk.filesystem) -// console.info('Total space:', disk.total) -// console.info('Used space:', disk.used) -// console.info('Available space:', disk.available) -// console.info('Available space (pourcent):', disk.available_pourcent) -// console.info('Mountpoint:', disk.mountpoint) -// }) -// ``` - -// Here is how the output may look like: - -// ```json -// [ { filesystem: '/dev/sda1', -// total: '8255928', -// used: '1683700', -// available: '6152852', -// available_pourcent: '22%', -// mountpoint: '/' }, -// { filesystem: 'tmpfs', -// total: '961240', -// used: '0', -// available: '961240', -// available_pourcent: '0%', -// mountpoint: '/dev/shm' } ] -// ``` - -// ## Schema definitions -var definitions, handler, utils; - -definitions = { - config: { - type: 'object', - properties: { - 'output': { - type: 'array', - default: ['source', 'fstype', 'itotal', 'iused', 'iavail', 'ipcent', 'size', 'used', 'avail', 'pcent', 'target'], - items: { - type: 'string', - enum: ['source', 'fstype', 'itotal', 'iused', 'iavail', 'ipcent', 'size', 'used', 'avail', 'pcent', 'target'] //todo use and test $ref /properties/output/default - }, - description: `The list of properties to be returned, default to all of them.` - } - } - } -}; - -// ## Handler -handler = async function({config}, callback) { - var disk, disks, i, line, property, stdout; - ({stdout} = (await this.execute({ - command: `df --output='${config.output.join(',')}'` - }))); - disks = (function() { - var j, k, len, len1, ref, ref1, results; - ref = utils.string.lines(stdout); - results = []; - for (i = j = 0, len = ref.length; j < len; i = ++j) { - line = ref[i]; - if (i === 0) { - continue; - } - if (/^\s*$/.test(line)) { - continue; - } - line = line.split(/\s+/); - disk = { - df: {} - }; - ref1 = config.output; - for (i = k = 0, len1 = ref1.length; k < len1; i = ++k) { - property = ref1[i]; - disk.df[property] = line[i]; - } - disk.filesystem = disk.df.source; - disk.total = disk.df.itotal * 1024; - disk.used = disk.df.iused * 1024; - disk.available = disk.df.avail * 1024; - disk.available_pourcent = disk.df.pcent; - disk.mountpoint = disk.df.target; - results.push(disk); - } - return results; - })(); - return { - disks: disks - }; -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - definitions: definitions, - shy: true - } -}; - -// ## Dependencies -utils = require('../utils'); diff --git a/packages/system/src/info/disks.coffee.md b/packages/system/lib/info/disks/README.md similarity index 70% rename from packages/system/src/info/disks.coffee.md rename to packages/system/lib/info/disks/README.md index 754be0bab..8d70752c3 100644 --- a/packages/system/src/info/disks.coffee.md +++ b/packages/system/lib/info/disks/README.md @@ -104,58 +104,3 @@ Here is how the output may look like: available_pourcent: '0%', mountpoint: '/dev/shm' } ] ``` - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'output': - type: 'array' - default: [ - 'source', 'fstype', 'itotal', 'iused', 'iavail', 'ipcent', 'size', - 'used', 'avail', 'pcent', 'target' - ] - items: - type: 'string' - enum: [ #todo use and test $ref /properties/output/default - 'source', 'fstype', 'itotal', 'iused', 'iavail', 'ipcent', 'size', - 'used', 'avail', 'pcent', 'target' - ] - description: ''' - The list of properties to be returned, default to all of them. - ''' - -## Handler - - handler = ({config}, callback) -> - {stdout} = await @execute - command: "df --output='#{config.output.join ','}'" - disks = for line, i in utils.string.lines stdout - continue if i is 0 - continue if /^\s*$/.test line - line = line.split /\s+/ - disk = {df: {}} - for property, i in config.output - disk.df[property] = line[i] - disk.filesystem = disk.df.source - disk.total = disk.df.itotal * 1024 - disk.used = disk.df.iused * 1024 - disk.available = disk.df.avail * 1024 - disk.available_pourcent = disk.df.pcent - disk.mountpoint = disk.df.target - disk - disks: disks - -## Exports - - module.exports = - handler: handler - metadata: - definitions: definitions - shy: true - -## Dependencies - - utils = require '../utils' diff --git a/packages/system/lib/info/disks/index.js b/packages/system/lib/info/disks/index.js new file mode 100644 index 000000000..7fcf1dc22 --- /dev/null +++ b/packages/system/lib/info/disks/index.js @@ -0,0 +1,39 @@ +// Dependencies +const utils = require("../../utils"); +const definitions = require("./schema.json"); + +// Action +module.exports = { + handler: async function ({ config }) { + const { stdout } = await this.execute({ + command: `df --output='${config.output.join(",")}'`, + }); + const disks = utils.string + .lines(stdout) + .filter((line, i) => i !== 0 && !/^\s*$/.test(line)) + .map((line) => { + const record = line.split(/\s+/); + const disk = { + df: {}, + }; + for (const i in config.output) { + const property = config.output[i]; + disk.df[property] = record[i]; + } + disk.filesystem = disk.df.source; + disk.total = disk.df.itotal * 1024; + disk.used = disk.df.iused * 1024; + disk.available = disk.df.avail * 1024; + disk.available_pourcent = disk.df.pcent; + disk.mountpoint = disk.df.target; + return disk; + }); + return { + disks: disks, + }; + }, + metadata: { + definitions: definitions, + shy: true, + }, +}; diff --git a/packages/system/lib/info/disks/schema.json b/packages/system/lib/info/disks/schema.json new file mode 100644 index 000000000..36b549ab1 --- /dev/null +++ b/packages/system/lib/info/disks/schema.json @@ -0,0 +1,40 @@ +{ + "config": { + "type": "object", + "properties": { + "output": { + "type": "array", + "default": [ + "source", + "fstype", + "itotal", + "iused", + "iavail", + "ipcent", + "size", + "used", + "avail", + "pcent", + "target" + ], + "items": { + "type": "string", + "enum": [ + "source", + "fstype", + "itotal", + "iused", + "iavail", + "ipcent", + "size", + "used", + "avail", + "pcent", + "target" + ] + }, + "description": "The list of properties to be returned, default to all of them." + } + } + } +} diff --git a/packages/system/lib/info/os.js b/packages/system/lib/info/os.js deleted file mode 100644 index c089c10d5..000000000 --- a/packages/system/lib/info/os.js +++ /dev/null @@ -1,86 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.info.os` - -// Expose system information. Internally, it uses the command `uname` to retrieve -// information. - -// ## Todo - -// There are more properties exposed by `uname` such as the machine hardware name -// and the hardware platform. Those properties shall be exposed. - -// We shall explain what "non-portable" means. - -// ## Example - -// ```js -// const {os} = await nikita.system.info.os() -// console.info('Architecture:', os.arch) -// console.info('Distribution:', os.distribution) -// console.info('Version:', os.version) -// console.info('Linux version:', os.linux_version) -// ``` - -// ## Schema definitions - -// There is no config for this action. -var definitions, handler, utils; - -definitions = { - 'output': { - type: 'object', - properties: { - 'os': { - type: 'object', - properties: { - 'arch': { - type: 'string', - description: `Print the machine architecte, eg \`x86_64\`, same as \`uname -m\`.` - }, - 'distribution': { - type: 'string', - description: `Linux distribution. Current values include 'rhel', 'centos', -'ubuntu', 'debian' and 'arch'.` - }, - 'version': { - type: 'string', - description: `Version of the distribution, for example '6.10' on CENTOS 6 or -\`7.9.2009\` on CENTOS 7.` - }, - 'linux_version': { - type: 'string', - description: `Linux kernel version, extracted from \`uname -r\`.` - } - } - } - } - } -}; - -// ## Handler -handler = async function() { - var arch, distribution, linux_version, stdout, version; - // Using `utils.os.command` to be consistant with OS conditions from core - ({stdout} = (await this.execute(utils.os.command))); - [arch, distribution, version, linux_version] = stdout.split('|'); - return { - os: { - arch: arch, - distribution: distribution, - version: version.length ? version : void 0, // eg Arch Linux - linux_version: linux_version - } - }; -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - definitions: definitions, - shy: true - } -}; - -// ## Dependencies -utils = require('../utils'); diff --git a/packages/system/lib/info/os/README.md b/packages/system/lib/info/os/README.md new file mode 100644 index 000000000..87d16f991 --- /dev/null +++ b/packages/system/lib/info/os/README.md @@ -0,0 +1,22 @@ + +# `nikita.system.info.os` + +Expose system information. Internally, it uses the command `uname` to retrieve +information. + +## Todo + +There are more properties exposed by `uname` such as the machine hardware name +and the hardware platform. Those properties shall be exposed. + +We shall explain what "non-portable" means. + +## Example + +```js +const {os} = await nikita.system.info.os() +console.info('Architecture:', os.arch) +console.info('Distribution:', os.distribution) +console.info('Version:', os.version) +console.info('Linux version:', os.linux_version) +``` diff --git a/packages/system/lib/info/os/index.js b/packages/system/lib/info/os/index.js new file mode 100644 index 000000000..f2afb7987 --- /dev/null +++ b/packages/system/lib/info/os/index.js @@ -0,0 +1,24 @@ +// Dependencies +const utils = require("../../utils"); +const definitions = require("./schema.json"); + +// Action +module.exports = { + handler: async function () { + // Using `utils.os.command` to be consistant with OS conditions plugin in core + const { stdout } = await this.execute(utils.os.command); + const [arch, distribution, version, linux_version] = stdout.split("|"); + return { + os: { + arch: arch, + distribution: distribution, + version: version.length ? version : void 0, // eg Arch Linux + linux_version: linux_version, + }, + }; + }, + metadata: { + definitions: definitions, + shy: true, + }, +}; diff --git a/packages/system/lib/info/os/schema.json b/packages/system/lib/info/os/schema.json new file mode 100644 index 000000000..8695f8902 --- /dev/null +++ b/packages/system/lib/info/os/schema.json @@ -0,0 +1,28 @@ +{ + "output": { + "type": "object", + "properties": { + "os": { + "type": "object", + "properties": { + "arch": { + "type": "string", + "description": "Print the machine architecte, eg `x86_64`, same as `uname -m`." + }, + "distribution": { + "type": "string", + "description": "Linux distribution. Current values include 'rhel', 'centos',\n'ubuntu', 'debian' and 'arch'." + }, + "version": { + "type": "string", + "description": "Version of the distribution, for example '6.10' on CENTOS 6 or\n`7.9.2009` on CENTOS 7." + }, + "linux_version": { + "type": "string", + "description": "Linux kernel version, extracted from `uname -r`." + } + } + } + } + } +} diff --git a/packages/system/lib/limits.js b/packages/system/lib/limits.js deleted file mode 100644 index 2c404dc75..000000000 --- a/packages/system/lib/limits.js +++ /dev/null @@ -1,418 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.limits` - -// Control system limits for a user. - -// ## Implementation strategy - -// ### `nproc` and `nofile` - -// There are two cases, depending on the specified value: - -// 1. Integer value -// If an int value is specified, then nikita checks that the value is lesser than -// the kernel limit. Please be aware that it is necessary but not sufficient to -// guarantee that the user would be able to open session. -// 2. Boolean value -// If a true value is specified, then nikita set it to 75% of the kernel limit. -// This value is neither optimal nor able to guarantee that the user would be -// able to open session, but that is the best nikita can automatically do. - -// ### Other values - -// Other values are not assessed by default. They must be integers. - -// ## Ulimit - -// Linux allows to limit the resources allocated to users or user groups via -// "/etc/security/limits.conf" and "/etc/security/limits.d/*.conf" files loaded by -// WFP (Plugable Authentication Module) at each logon. The user can then adapt the -// resources available to its needs via "ulimit". - -// It is possible to define, for a number of resources (number of open files, file size, -// number of instantiated process, CPU time, etc.), a "soft" limit which can be -// increased by user, via "ulimit" until a maximum "hard" limit. -// The system does not exceed the value of the soft limit. If the user wants to push -// this limit, it will set a new soft limit with ulimit. -// The soft limit is always lower or equal to the hard limit. -// In general, the limits applied to a user override those applied to a group. - -// ## Ulimit commands - -// The "S" option to "ulimit" impact the effective limit ("soft" limit) and the "H" -// impact the "hard" limit (maximum value that can be defined by the user). - -// | resource | soft | hard | unit | -// |----------------------|------------|------------|---------| -// | core file size | ulimit -Sc | ulimit -Hc | blocks | -// | data seg size | ulimit -Sd | ulimit -Hd | kbytes | -// | scheduling priority | ulimit -Se | ulimit -He | | -// | file size | ulimit -Sf | ulimit -Hf | blocks | -// | max locked memory | ulimit -Sl | ulimit -Hl | kbytes | -// | pending signals | ulimit -Si | ulimit -Hi | | -// | max memory size | ulimit -Sm | ulimit -Hm | kbytes | -// | open files | ulimit -Sn | ulimit -Hn | | -// | pipe size | ulimit -Sp | ulimit -Hp | bytes | -// | POSIX message queues | ulimit -Sq | ulimit -Hq | bytes | -// | real-time priority | ulimit -Sr | ulimit -Hr | | -// | stack size | ulimit -Ss | ulimit -Hs | kbytes | -// | cpu time | ulimit -St | ulimit -Ht | seconds | -// | max user processes | ulimit -Su | ulimit -Hu | | -// | virtual memory | ulimit -Sv | ulimit -Hv | kbytes | -// | file locks | ulimit -Sx | ulimit -Hx | | - -// Pass the option in flag-mode to get, and follows it with a value to set. - -// ## Retrieve current information - -// Number of sub-process for a process: - -// ```bash -// pid=14986 -// ls /proc/$pid/task | wc -// ps -L p $pid --no-headers | wc -l -// ``` - -// Number of sub-process for a user, the option "-L" show threads, possibly with -// LWP and NLWP columns: - -// ```bash -// user=`whoami` -// ps -L -u $user --no-headers | wc -l -// ``` - -// ## Kernel Limits - -// User limits cannot exceed kernel limits, so you need to configure kernel limits -// before user limits. - -// ### Processes - -// ```bash -// sysctl kernel.pid_max # print kernel.pid_max = VALUE -// cat /proc/sys/kernel/pid_max # print VALUE -// ``` - -// _Temporary change_: `echo 4194303 > /proc/sys/kernel/pid_max` - -// _Permanent change_: `vi /etc/sysctl.conf # kernel.pid_max = 4194303` - -// ### Open Files - -// ```bash -// sysctl fs.file-max # print fs.file-max = VALUE -// cat /proc/sys/fs/file-max # print VALUE -// ``` - -// _Temporary change_: `echo 1631017 > /proc/sys/fs/file-max` - -// _Permanent change_ : `vi /etc/sysctl.conf # fs.file-max = 1631017` - -// ## Example - -// Setting the number of open file descriptors to .75 of the maximum value for -// all the users: - -// ```js -// const {$status} = await nikita.system.limits({ -// system: true, -// nofile: true -// }); -// console.log(`Limits modified: ${$status}`); -// ``` - -// ## Callback parameters - -// * `$status` -// Value is "true" if limits configuration file has been modified. - -// ## Schema definitions - -// Refer to the [limits.conf(5)](https://linux.die.net/man/5/limits.conf) Linux man -// page for further information. -var definitions, handler, regexp; - -definitions = { - config: { - type: 'object', - properties: { - 'as': { - $ref: '#/definitions/limits', - description: `Address space limit (KB).` - }, - 'core': { - $ref: '#/definitions/limits', - description: `Limits the core file size (KB).` - }, - 'cpu': { - $ref: '#/definitions/limits', - description: `CPU time limit (in seconds). When the process reaches the soft limit, -it receives a SIGXCPU every second. When it reaches the hard limit, it -receives SIGKILL.` - }, - 'data': { - $ref: '#/definitions/limits', - description: `Max data size (KB).` - }, - 'fsize': { - $ref: '#/definitions/limits', - description: `Maximum filesize (KB).` - }, - 'locks': { - $ref: '#/definitions/limits', - description: `Max number of file locks the user can hold.` - }, - 'maxlogins': { - $ref: '#/definitions/limits', - description: `Max number of logins for this user.` - }, - 'maxsyslogins': { - $ref: '#/definitions/limits', - description: `Max number of logins on the system.` - }, - 'memlock': { - $ref: '#/definitions/limits', - description: `Max locked-in-memory address space (KB).` - }, - 'msgqueue': { - $ref: '#/definitions/limits', - description: `Max memory used by POSIX message queues (bytes).` - }, - 'nice': { - oneOf: [ - { - type: 'integer', - minimum: -20, - maximum: 19 - }, - { - type: 'object', - patternProperties: { - '^-|soft|hard$': { - type: 'integer', - minimum: -20, - maximum: 19 - } - }, - additionalProperties: false - } - ], - description: `Max nice priority allowed to raise to values.` - }, - 'nofile': { - $ref: '#/definitions/limits', - description: `Max number of open file descriptors.` - }, - 'nproc': { - $ref: '#/definitions/limits', - description: `Max number of processes.` - }, - 'priority': { - oneOf: [ - { - type: 'integer' - }, - { - type: 'object', - patternProperties: { - '^-|soft|hard$': { - type: 'integer' - } - }, - additionalProperties: false - } - ], - description: `Priority to run user process with.` - }, - 'rss': { - $ref: '#/definitions/limits', - description: `Max resident set size (KB).` - }, - 'sigpending': { - $ref: '#/definitions/limits', - description: `Max number of pending signals.` - }, - 'stack': { - $ref: '#/definitions/limits', - description: `Max stack size (KB).` - }, - 'rtprio': { - $ref: '#/definitions/limits', - description: `Max realtime priority..` - }, - 'system': { - type: 'boolean', - description: `Apply the limits at the system level.` - }, - 'target': { - type: 'string', - description: `Where to write the file, default to "/etc/security/limits.conf" for -system limits and "/etc/security/limits.d/#{config.user}.conf" for -user limits.` - }, - 'user': { - type: 'string', - description: `The username to who the limit apply, also used for the default target -name.` - } - }, - oneOf: [ - { - required: ['system'] - }, - { - required: ['user'] - } - ] - }, - 'limits': { - anyOf: [ - { - type: ['boolean', - 'integer'] - }, - { - type: 'object', - patternProperties: { - '^-|soft|hard$': { - anyOf: [ - { - type: ['boolean', - 'integer'] - }, - { - type: 'string', - enum: ['unlimited'] - } - ] - } - }, - additionalProperties: false - }, - { - type: 'string', - enum: ['unlimited'] - } - ] - } -}; - -// ## Handler -handler = async function({config}) { - var _, i, j, k, kern_limit, len, len1, opt, ref, ref1, ref2, ref3, v, write; - if (config.system && config.user) { - throw Error(`Incoherent config: both system and user configuration are defined, ${JSON.stringify({ - system: config.system, - user: config.user - })}`); - } - if (config.system) { - config.user = '*'; - } - if (!config.user) { - throw Error("Missing required option 'user'"); - } - if (config.target == null) { - config.target = "/etc/security/" + (config.user === '*' ? "limits.conf" : `limits.d/${config.user}.conf`); - } - // Calculate nofile from kernel limit - if (config.nofile != null) { - ({ - stdout: kern_limit - } = (await this.execute({ - command: "cat /proc/sys/fs/file-max", - // shy: true - trim: true - }))); - if (config.nofile === true) { - config.nofile = Math.round(kern_limit * 0.75); - } else if (typeof config.nofile === 'number') { - if (config.nofile >= kern_limit) { - throw Error(`Invalid nofile configuration property. Please set int value lesser than kernel limit: ${kern_limit}`); - } - } else if (typeof config.nofile === 'object') { - ref = config.nofile; - for (_ in ref) { - v = ref[_]; - if (v >= kern_limit) { - throw Error(`Invalid nofile configuration property. Please set int value lesser than kernel limit: ${kern_limit}`); - } - } - } - } - // Calculate nproc from kernel limit - if (config.nproc != null) { - ({ - stdout: kern_limit - } = (await this.execute({ - command: "cat /proc/sys/kernel/pid_max", - shy: true, - trim: true - }))); - if (config.nproc === true) { - config.nproc = Math.round(kern_limit * 0.75); - } else if (typeof config.nproc === 'number') { - if (config.nproc >= kern_limit) { - throw Error(`Invalid nproc configuration property. Please set int value lesser than kernel limit: ${kern_limit}`); - } - } else if (typeof config.nproc === 'object') { - ref1 = config.nproc; - for (_ in ref1) { - v = ref1[_]; - if (v >= kern_limit) { - throw Error(`Invalid nproc configuration property. Please set int value lesser than kernel limit: ${kern_limit}`); - } - } - } - } - // Config normalization - write = []; - ref2 = ['as', 'core', 'cpu', 'data', 'fsize', 'locks', 'maxlogins', 'maxsyslogins', 'memlock', 'msgqueue', 'nice', 'nofile', 'nproc', 'priority', 'rss', 'sigpending', 'stack', 'rtprio']; - for (i = 0, len = ref2.length; i < len; i++) { - opt = ref2[i]; - if (config[opt] == null) { - continue; - } - if (typeof config[opt] !== 'object') { - config[opt] = { - '-': config[opt] - }; - } - ref3 = Object.keys(config[opt]); - for (j = 0, len1 = ref3.length; j < len1; j++) { - k = ref3[j]; - if (k !== 'soft' && k !== 'hard' && k !== '-') { - throw Error(`Invalid option: ${JSON.stringify(config[opt])}`); - } - if (!((typeof config[opt][k] === 'number') || config[opt][k] === 'unlimited')) { - throw Error(`Invalid option: ${config[opt][k]} not a number`); - } - write.push({ - match: RegExp(`^${regexp.escape(config.user)} +${regexp.escape(k)} +${opt}.+$`, 'm'), - replace: `${config.user} ${k} ${opt} ${config[opt][k]}`, - append: true - }); - } - } - if (!write.length) { - return false; - } - return this.file({ - target: config.target, - write: write, - eof: true, - uid: config.uid, - gid: config.gid - }); -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - definitions: definitions - } -}; - -// ## Dependencies -({regexp} = require('./utils')); diff --git a/packages/system/lib/limits/README.md b/packages/system/lib/limits/README.md new file mode 100644 index 000000000..07b2b9cd9 --- /dev/null +++ b/packages/system/lib/limits/README.md @@ -0,0 +1,124 @@ + +# `nikita.system.limits` + +Control system limits for a user. + +## Implementation strategy + +### `nproc` and `nofile` + +There are two cases, depending on the specified value: + +1. Integer value + If an int value is specified, then nikita checks that the value is lesser than + the kernel limit. Please be aware that it is necessary but not sufficient to + guarantee that the user would be able to open session. +2. Boolean value + If a true value is specified, then nikita set it to 75% of the kernel limit. + This value is neither optimal nor able to guarantee that the user would be + able to open session, but that is the best nikita can automatically do. + +### Other values + +Other values are not assessed by default. They must be integers. + +## Ulimit + +Linux allows to limit the resources allocated to users or user groups via +"/etc/security/limits.conf" and "/etc/security/limits.d/*.conf" files loaded by +WFP (Plugable Authentication Module) at each logon. The user can then adapt the +resources available to its needs via "ulimit". Refer to the +[limits.conf(5)](https://linux.die.net/man/5/limits.conf) Linux man page for +further information. + +It is possible to define, for a number of resources (number of open files, file size, +number of instantiated process, CPU time, etc.), a "soft" limit which can be +increased by user, via "ulimit" until a maximum "hard" limit. +The system does not exceed the value of the soft limit. If the user wants to push +this limit, it will set a new soft limit with ulimit. +The soft limit is always lower or equal to the hard limit. +In general, the limits applied to a user override those applied to a group. + +## Ulimit commands + +The "S" option to "ulimit" impact the effective limit ("soft" limit) and the "H" +impact the "hard" limit (maximum value that can be defined by the user). + +| resource | soft | hard | unit | +|----------------------|------------|------------|---------| +| core file size | ulimit -Sc | ulimit -Hc | blocks | +| data seg size | ulimit -Sd | ulimit -Hd | kbytes | +| scheduling priority | ulimit -Se | ulimit -He | | +| file size | ulimit -Sf | ulimit -Hf | blocks | +| max locked memory | ulimit -Sl | ulimit -Hl | kbytes | +| pending signals | ulimit -Si | ulimit -Hi | | +| max memory size | ulimit -Sm | ulimit -Hm | kbytes | +| open files | ulimit -Sn | ulimit -Hn | | +| pipe size | ulimit -Sp | ulimit -Hp | bytes | +| POSIX message queues | ulimit -Sq | ulimit -Hq | bytes | +| real-time priority | ulimit -Sr | ulimit -Hr | | +| stack size | ulimit -Ss | ulimit -Hs | kbytes | +| cpu time | ulimit -St | ulimit -Ht | seconds | +| max user processes | ulimit -Su | ulimit -Hu | | +| virtual memory | ulimit -Sv | ulimit -Hv | kbytes | +| file locks | ulimit -Sx | ulimit -Hx | | + +Pass the option in flag-mode to get, and follows it with a value to set. + +## Retrieve current information + +Number of sub-process for a process: + +```bash +pid=14986 +ls /proc/$pid/task | wc +ps -L p $pid --no-headers | wc -l +``` + +Number of sub-process for a user, the option "-L" show threads, possibly with +LWP and NLWP columns: + +```bash +user=`whoami` +ps -L -u $user --no-headers | wc -l +``` + +## Kernel Limits + +User limits cannot exceed kernel limits, so you need to configure kernel limits +before user limits. + +### Processes + +```bash +sysctl kernel.pid_max # print kernel.pid_max = VALUE +cat /proc/sys/kernel/pid_max # print VALUE +``` + +_Temporary change_: `echo 4194303 > /proc/sys/kernel/pid_max` + +_Permanent change_: `vi /etc/sysctl.conf # kernel.pid_max = 4194303` + +### Open Files + +```bash +sysctl fs.file-max # print fs.file-max = VALUE +cat /proc/sys/fs/file-max # print VALUE +``` + +_Temporary change_: `echo 1631017 > /proc/sys/fs/file-max` + +_Permanent change_ : `vi /etc/sysctl.conf # fs.file-max = 1631017` + +## Example + +Setting the number of open file descriptors to .75 of the maximum value for +all the users: + +```js +const {$status} = await nikita.system.limits({ + system: true, + nofile: true +}); +console.log(`Limits modified: ${$status}`); +``` diff --git a/packages/system/lib/limits/index.js b/packages/system/lib/limits/index.js new file mode 100644 index 000000000..5a42dbe75 --- /dev/null +++ b/packages/system/lib/limits/index.js @@ -0,0 +1,141 @@ +// Dependencies +const { regexp } = require("../utils"); +const definitions = require("./schema.json"); + +// Action +module.exports = { + handler: async function ({ config }) { + if (config.system && config.user) { + throw Error( + `Incoherent config: both system and user configuration are defined, ${JSON.stringify( + { + system: config.system, + user: config.user, + } + )}` + ); + } + if (config.system) { + config.user = "*"; + } + if (!config.user) { + throw Error("Missing required option 'user'"); + } + if (config.target == null) { + config.target = + "/etc/security/" + + (config.user === "*" ? "limits.conf" : `limits.d/${config.user}.conf`); + } + // Calculate nofile from kernel limit + if (config.nofile != null) { + const { stdout: kern_limit } = await this.execute({ + command: "cat /proc/sys/fs/file-max", + // shy: true + trim: true, + }); + if (config.nofile === true) { + config.nofile = Math.round(kern_limit * 0.75); + } else if (typeof config.nofile === "number") { + if (config.nofile >= kern_limit) { + throw Error( + `Invalid nofile configuration property. Please set int value lesser than kernel limit: ${kern_limit}` + ); + } + } else if (typeof config.nofile === "object") { + Object.values(config.nofile).filter(v => v >= kern_limit).forEach((v) => { + throw Error(`Invalid nofile configuration property. Please set int value lesser than kernel limit: ${kern_limit}`); + }); + } + } + // Calculate nproc from kernel limit + if (config.nproc != null) { + const { stdout: kern_limit } = await this.execute({ + command: "cat /proc/sys/kernel/pid_max", + shy: true, + trim: true, + }); + if (config.nproc === true) { + config.nproc = Math.round(kern_limit * 0.75); + } else if (typeof config.nproc === "number") { + if (config.nproc >= kern_limit) { + throw Error( + `Invalid nproc configuration property. Please set int value lesser than kernel limit: ${kern_limit}` + ); + } + } else if (typeof config.nproc === "object") { + for (const v of config.nproc) { + if (v >= kern_limit) { + throw Error( + `Invalid nproc configuration property. Please set int value lesser than kernel limit: ${kern_limit}` + ); + } + } + } + } + // Config normalization + const write = []; + for (const opt of [ + "as", + "core", + "cpu", + "data", + "fsize", + "locks", + "maxlogins", + "maxsyslogins", + "memlock", + "msgqueue", + "nice", + "nofile", + "nproc", + "priority", + "rss", + "sigpending", + "stack", + "rtprio", + ]) { + if (config[opt] == null) { + continue; + } + if (typeof config[opt] !== "object") { + config[opt] = { + "-": config[opt], + }; + } + for (const k of Object.keys(config[opt])) { + if (k !== "soft" && k !== "hard" && k !== "-") { + throw Error(`Invalid option: ${JSON.stringify(config[opt])}`); + } + if ( + !( + typeof config[opt][k] === "number" || config[opt][k] === "unlimited" + ) + ) { + throw Error(`Invalid option: ${config[opt][k]} not a number`); + } + write.push({ + match: RegExp( + `^${regexp.escape(config.user)} +${regexp.escape(k)} +${opt}.+$`, + "m" + ), + replace: `${config.user} ${k} ${opt} ${config[opt][k]}`, + append: true, + }); + } + } + if (!write.length) { + return false; + } + const {$status} = await this.file({ + target: config.target, + write: write, + eof: true, + uid: config.uid, + gid: config.gid, + }); + return $status + }, + metadata: { + definitions: definitions, + }, +}; diff --git a/packages/system/lib/limits/schema.json b/packages/system/lib/limits/schema.json new file mode 100644 index 000000000..116512fe6 --- /dev/null +++ b/packages/system/lib/limits/schema.json @@ -0,0 +1,171 @@ +{ + "config": { + "type": "object", + "properties": { + "as": { + "$ref": "#/definitions/limits", + "description": "Address space limit (KB)." + }, + "core": { + "$ref": "#/definitions/limits", + "description": "Limits the core file size (KB)." + }, + "cpu": { + "$ref": "#/definitions/limits", + "description": "CPU time limit (in seconds). When the process reaches the soft limit,\nit receives a SIGXCPU every second. When it reaches the hard limit, it\nreceives SIGKILL." + }, + "data": { + "$ref": "#/definitions/limits", + "description": "Max data size (KB)." + }, + "fsize": { + "$ref": "#/definitions/limits", + "description": "Maximum filesize (KB)." + }, + "locks": { + "$ref": "#/definitions/limits", + "description": "Max number of file locks the user can hold." + }, + "maxlogins": { + "$ref": "#/definitions/limits", + "description": "Max number of logins for this user." + }, + "maxsyslogins": { + "$ref": "#/definitions/limits", + "description": "Max number of logins on the system." + }, + "memlock": { + "$ref": "#/definitions/limits", + "description": "Max locked-in-memory address space (KB)." + }, + "msgqueue": { + "$ref": "#/definitions/limits", + "description": "Max memory used by POSIX message queues (bytes)." + }, + "nice": { + "oneOf": [ + { + "type": "integer", + "minimum": -20, + "maximum": 19 + }, + { + "type": "object", + "patternProperties": { + "^-|soft|hard$": { + "type": "integer", + "minimum": -20, + "maximum": 19 + } + }, + "additionalProperties": false + } + ], + "description": "Max nice priority allowed to raise to values." + }, + "nofile": { + "$ref": "#/definitions/limits", + "description": "Max number of open file descriptors." + }, + "nproc": { + "$ref": "#/definitions/limits", + "description": "Max number of processes." + }, + "priority": { + "oneOf": [ + { + "type": "integer" + }, + { + "type": "object", + "patternProperties": { + "^-|soft|hard$": { + "type": "integer" + } + }, + "additionalProperties": false + } + ], + "description": "Priority to run user process with." + }, + "rss": { + "$ref": "#/definitions/limits", + "description": "Max resident set size (KB)." + }, + "sigpending": { + "$ref": "#/definitions/limits", + "description": "Max number of pending signals." + }, + "stack": { + "$ref": "#/definitions/limits", + "description": "Max stack size (KB)." + }, + "rtprio": { + "$ref": "#/definitions/limits", + "description": "Max realtime priority.." + }, + "system": { + "type": "boolean", + "description": "Apply the limits at the system level." + }, + "target": { + "type": "string", + "description": "Where to write the file, default to \"/etc/security/limits.conf\" for\nsystem limits and \"/etc/security/limits.d/#{config.user}.conf\" for\nuser limits." + }, + "user": { + "type": "string", + "description": "The username to who the limit apply, also used for the default target\nname." + } + }, + "oneOf": [ + { + "required": [ + "system" + ] + }, + { + "required": [ + "user" + ] + } + ] + }, + "limits": { + "anyOf": [ + { + "type": [ + "boolean", + "integer" + ] + }, + { + "type": "object", + "patternProperties": { + "^-|soft|hard$": { + "anyOf": [ + { + "type": [ + "boolean", + "integer" + ] + }, + { + "type": "string", + "enum": [ + "unlimited" + ] + } + ] + } + }, + "additionalProperties": false + }, + { + "type": "string", + "enum": [ + "unlimited" + ] + } + ] + } +} diff --git a/packages/system/lib/mod.js b/packages/system/lib/mod.js deleted file mode 100644 index e9fbfddff..000000000 --- a/packages/system/lib/mod.js +++ /dev/null @@ -1,130 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.mod` - -// Load a kernel module. By default, unless the `persist` config is "false", -// module are loaded on reboot by writing the file "/etc/modules-load.d/{name}.conf". - -// ## Examples - -// Activate the module "vboxpci" in the file "/etc/modules-load.d/vboxpci.conf": - -// ``` -// nikita.system.mod({ -// modules: 'vboxpci' -// }) -// ``` - -// Activate the module "vboxpci" in the file "/etc/modules-load.d/my_modules.conf": - -// ``` -// nikita.system.mod({ -// target: 'my_modules.conf', -// modules: 'vboxpci' -// }); -// ``` - -// ## Hooks -var definitions, handler, on_action, path, quote; - -on_action = function({config}) { - if (typeof config.modules === 'string') { - return config.modules = { - [config.modules]: true - }; - } -}; - -// ## Schema definitions -definitions = { - config: { - type: 'object', - properties: { - 'load': { - type: 'boolean', - default: true, - description: `Load the module with \`modprobe\`.` - }, - 'modules': { - oneOf: [ - { - type: 'string' - }, - { - type: 'object', - patternProperties: { - '.*': { - type: 'boolean' - } - }, - additionalProperties: false - } - ], - description: `Names of the modules.` - }, - 'persist': { - type: 'boolean', - default: true, - description: `Load the module on startup by placing a file, see \`target\`.` - }, - 'target': { - type: 'string', - description: `Path of the file to write the module, relative to -"/etc/modules-load.d" unless absolute, default to -"/etc/modules-load.d/{config.modules}.conf".` - } - }, - required: ['modules'] - } -}; - -// ## Handler -handler = async function({metadata, config}) { - var active, module, ref, target; - ref = config.modules; - for (module in ref) { - active = ref[module]; - target = config.target; - if (target == null) { - target = `${module}.conf`; - } - target = path.resolve('/etc/modules-load.d', target); - await this.execute({ - $if: config.load && active, - command: `lsmod | grep ${module} && exit 3 -modprobe ${module}`, - code: [0, 3] - }); - await this.execute({ - $if: config.load && !active, - command: `lsmod | grep ${module} || exit 3 -modprobe -r ${module}`, - code: [0, 3] - }); - await this.file({ - $if: config.persist, - target: target, - match: RegExp(`^${quote(module)}(\\n|$)`, "mg"), - replace: active ? `${module}\n` : '', - append: true, - eof: true - }); - } - return void 0; -}; - -// ## Exports -module.exports = { - handler: handler, - hooks: { - on_action: on_action - }, - metadata: { - definitions: definitions, - argument_to_config: 'modules' - } -}; - -// ## Dependencies -path = require('path'); - -quote = require('regexp-quote'); diff --git a/packages/system/lib/mod/README.md b/packages/system/lib/mod/README.md new file mode 100644 index 000000000..d91914ce1 --- /dev/null +++ b/packages/system/lib/mod/README.md @@ -0,0 +1,24 @@ + +# `nikita.system.mod` + +Load a kernel module. By default, unless the `persist` config is "false", +module are loaded on reboot by writing the file "/etc/modules-load.d/{name}.conf". + +## Examples + +Activate the module "vboxpci" in the file "/etc/modules-load.d/vboxpci.conf": + +``` +nikita.system.mod({ + modules: 'vboxpci' +}) +``` + +Activate the module "vboxpci" in the file "/etc/modules-load.d/my_modules.conf": + +``` +nikita.system.mod({ + target: 'my_modules.conf', + modules: 'vboxpci' +}); +``` diff --git a/packages/system/lib/mod/index.js b/packages/system/lib/mod/index.js new file mode 100644 index 000000000..df4c8d1ae --- /dev/null +++ b/packages/system/lib/mod/index.js @@ -0,0 +1,57 @@ + +// Dependencies +const path = require('path'); +const quote = require('regexp-quote'); +const dedent = require('dedent'); +const definitions = require('./schema.json'); + +// Action +module.exports = { + handler: async function({metadata, config}) { + for (const module in config.modules) { + const active = config.modules[module]; + let target = config.target; + if (target == null) { + target = `${module}.conf`; + } + target = path.resolve('/etc/modules-load.d', target); + await this.execute({ + $if: config.load && active, + command: dedent` + lsmod | grep ${module} && exit 3 + modprobe ${module} + `, + code: [0, 3] + }); + await this.execute({ + $if: config.load && !active, + command: dedent` + lsmod | grep ${module} || exit 3 + modprobe -r ${module} + `, + code: [0, 3] + }); + await this.file({ + $if: config.persist, + target: target, + match: RegExp(`^${quote(module)}(\\n|$)`, "mg"), + replace: active ? `${module}\n` : '', + append: true, + eof: true + }); + } + }, + hooks: { + on_action: function({config}) { + if (typeof config.modules === 'string') { + config.modules = { + [config.modules]: true + }; + } + } + }, + metadata: { + definitions: definitions, + argument_to_config: 'modules' + } +}; diff --git a/packages/system/lib/mod/schema.json b/packages/system/lib/mod/schema.json new file mode 100644 index 000000000..e14a99996 --- /dev/null +++ b/packages/system/lib/mod/schema.json @@ -0,0 +1,41 @@ +{ + "config": { + "type": "object", + "properties": { + "load": { + "type": "boolean", + "default": true, + "description": "Load the module with `modprobe`." + }, + "modules": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "patternProperties": { + ".*": { + "type": "boolean" + } + }, + "additionalProperties": false + } + ], + "description": "Names of the modules." + }, + "persist": { + "type": "boolean", + "default": true, + "description": "Load the module on startup by placing a file, see `target`." + }, + "target": { + "type": "string", + "description": "Path of the file to write the module, relative to\n\"/etc/modules-load.d\" unless absolute, default to\n\"/etc/modules-load.d/{config.modules}.conf\"." + } + }, + "required": [ + "modules" + ] + } +} diff --git a/packages/system/lib/running.js b/packages/system/lib/running.js deleted file mode 100644 index 5886f653b..000000000 --- a/packages/system/lib/running.js +++ /dev/null @@ -1,159 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.running` - -// Check if a process is running. - -// ## Check if the pid is running - -// The example check if a pid match a running process. - -// ```js -// const {$status} = await nikita.system.running({ -// pid: 1034, -// }) -// console.info(`Is PID running: ${$status}`) -// ``` - -// ## Check if the pid stored in a file is running - -// The example read a file and check if the pid stored inside is currently running. -// This pattern is used by YUM and APT to create lock files. The target file will -// be removed if it stores a value not matching a running pid. - -// ```js -// const {$status} = await nikita.system.running({ -// target: '/var/run/yum.pid' -// }) -// console.info(`Is PID running: ${$status}`) -// ``` - -// ## Hooks -var definitions, handler, on_action; - -on_action = function({config}) { - if (typeof config.pid === 'string') { - return config.pid = parseInt(config.pid, 10); - } -}; - -// ## Schema definitions -definitions = { - config: { - type: 'object', - properties: { - 'pid': { - type: 'integer', - description: `The PID of the process to inspect, required if \`target\` is not provided.` - }, - 'target': { - type: 'string', - description: `Path to the file storing the PID value, required if \`pid\` is not provided.` - } - }, - anyOf: [ - { - required: ['pid'] - }, - { - required: ['target'] - } - ] - } -}; - -// ## Handler -handler = async function({ - config, - tools: {log} - }) { - var code, stdout; - if (!((config.pid != null) || config.target)) { - // Validate parameters - throw Error('Invalid Options: one of pid or target must be provided'); - } - if ((config.pid != null) && config.target) { - throw Error('Invalid Options: either pid or target must be provided'); - } - if (config.pid) { - ({code} = (await this.execute({ - command: `kill -s 0 '${config.pid}' >/dev/null 2>&1 || exit 42`, - code: [0, 42] - }))); - log((function() { - switch (code) { - case 0: - return { - message: `PID ${config.pid} is running`, - level: 'INFO', - module: 'nikita/lib/system/running' - }; - case 42: - return { - message: `PID ${config.pid} is not running`, - level: 'INFO', - module: 'nikita/lib/system/running' - }; - } - })()); - if (code === 0) { - return { - running: true - }; - } - } - if (config.target) { - ({code, stdout} = (await this.execute({ - command: `[ -f '${config.target}' ] || exit 43 -pid=\`cat '${config.target}'\` -echo $pid -if ! kill -s 0 "$pid" >/dev/null 2>&1; then - rm '${config.target}'; - exit 42; -fi`, - code: [0, [42, 43]], - stdout_trim: true - }))); - log((function() { - switch (code) { - case 0: - return { - message: `PID ${stdout} is running`, - level: 'INFO', - module: 'nikita/lib/system/running' - }; - case 42: - return { - message: `PID ${stdout} is not running`, - level: 'INFO', - module: 'nikita/lib/system/running' - }; - case 43: - return { - message: `PID file ${config.target} does not exists`, - level: 'INFO', - module: 'nikita/lib/system/running' - }; - } - })()); - if (code === 0) { - return { - running: true - }; - } - } - return { - running: false - }; -}; - -// ## Exports -module.exports = { - handler: handler, - hooks: { - on_action: on_action - }, - metadata: { - // raw_output: true - definitions: definitions - } -}; diff --git a/packages/system/lib/running/README.md b/packages/system/lib/running/README.md new file mode 100644 index 000000000..98ec2e441 --- /dev/null +++ b/packages/system/lib/running/README.md @@ -0,0 +1,28 @@ + +# `nikita.system.running` + +Check if a process is running. + +## Check if the pid is running + +The example check if a pid match a running process. + +```js +const {$status} = await nikita.system.running({ + pid: 1034, +}) +console.info(`Is PID running: ${$status}`) +``` + +## Check if the pid stored in a file is running + +The example read a file and check if the pid stored inside is currently running. +This pattern is used by YUM and APT to create lock files. The target file will +be removed if it stores a value not matching a running pid. + +```js +const {$status} = await nikita.system.running({ + target: '/var/run/yum.pid' +}) +console.info(`Is PID running: ${$status}`) +``` diff --git a/packages/system/lib/running/index.js b/packages/system/lib/running/index.js new file mode 100644 index 000000000..c776851e0 --- /dev/null +++ b/packages/system/lib/running/index.js @@ -0,0 +1,83 @@ + +// Dependencies +const dedent = require('dedent'); +const definitions = require('./schema.json'); + +// Action +module.exports = { + handler: async function({ + config, + tools: {log} + }) { + if (!(config.pid != null || config.target)) { + // Validate parameters + throw Error('Invalid Options: one of pid or target must be provided'); + } + if ((config.pid != null) && config.target) { + throw Error('Invalid Options: either pid or target must be provided'); + } + if (config.pid) { + const {code} = await this.execute({ + command: `kill -s 0 '${config.pid}' >/dev/null 2>&1 || exit 42`, + code: [0, 42] + }); + log( + "INFO", + code === 0 + ? `PID ${config.pid} is running` + : code === 42 + ? `PID ${config.pid} is not running` + : undefined + ); + if (code === 0) { + return { + running: true + }; + } + } + if (config.target) { + const {code, stdout} = await this.execute({ + command: dedent` + [ -f '${config.target}' ] || exit 43 + pid=\`cat '${config.target}'\` + echo $pid + if ! kill -s 0 "$pid" >/dev/null 2>&1; then + rm '${config.target}'; + exit 42; + fi + `, + code: [0, [42, 43]], + stdout_trim: true + }); + log('INFO', (function() { + switch (code) { + case 0: + return `PID ${stdout} is running`; + case 42: + return `PID ${stdout} is not running`; + case 43: + return `PID file ${config.target} does not exists`; + } + })()); + if (code === 0) { + return { + running: true + }; + } + } + return { + running: false + }; + }, + hooks: { + on_action: function({config}) { + if (typeof config.pid === 'string') { + return config.pid = parseInt(config.pid, 10); + } + } + }, + metadata: { + // raw_output: true + definitions: definitions + } +}; diff --git a/packages/system/lib/running/schema.json b/packages/system/lib/running/schema.json new file mode 100644 index 000000000..20327641c --- /dev/null +++ b/packages/system/lib/running/schema.json @@ -0,0 +1,27 @@ +{ + "config": { + "type": "object", + "properties": { + "pid": { + "type": "integer", + "description": "The PID of the process to inspect, required if `target` is not provided." + }, + "target": { + "type": "string", + "description": "Path to the file storing the PID value, required if `pid` is not provided." + } + }, + "anyOf": [ + { + "required": [ + "pid" + ] + }, + { + "required": [ + "target" + ] + } + ] + } +} diff --git a/packages/system/lib/tmpfs.js b/packages/system/lib/tmpfs.js deleted file mode 100644 index 5f46a04c1..000000000 --- a/packages/system/lib/tmpfs.js +++ /dev/null @@ -1,173 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.tmpfs` - -// Mount a directory with tmpfs.d as a [tmpfs](https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html) configuration file. - -// ## Callback parameters - -// * `$status` -// Wheter the directory was mounted or already mounted. - -// # Example - -// All parameters can be omitted except type. nikita.tmpfs will ommit by replacing -// the undefined value as '-', which does apply the os default behavior. - -// Setting uid/gid to '-', make the os creating the target owned by root:root. - -// ## Schema definitions -var definitions, handler, merge, utils; - -definitions = { - config: { - type: 'object', - properties: { - 'age': { - type: 'string', - description: `Used to decide what files to delete when cleaning.` - }, - 'argument': { - type: 'string', - description: `The destination path of the symlink if type is \`L\`.` - }, - 'backup': { - type: ['boolean', 'string'], - default: true, - description: `Create a backup, append a provided string to the filename extension or -a timestamp if value is not a string, only apply if the target file -exists and is modified.` - }, - 'gid': { - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid', - description: `File group name or group id.` - }, - 'merge': { - type: 'boolean', - default: true, - description: `Overrides properties if already exits.` - }, - 'mount': { - type: 'string', - description: `The mount point dir to create on system startup.` - }, - 'mode': { - $ref: 'module://@nikitajs/core/lib/actions/fs/chmod#/definitions/config/properties/mode', - description: `Mode of the target configuration file` - }, - 'name': { - type: 'string', - description: `The file name, can not be used with target. If only \`name\` is set, it -writes the content to default configuration directory and creates the -file as '\`name\`.conf'.` - }, - 'perm': { - type: 'string', - default: '0644', - description: `Mount path mode in string format like \`"0644"\`.` - }, - 'target': { - type: 'string', - description: `File path where to write content to. Defined to -/etc/tmpfiles.d/{config.uid}.conf if uid is defined or -/etc/tmpfiles.d/default.conf.` - }, - 'uid': { - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid', - description: `File user name or user id.` - } - }, - required: ['mount'] - } -}; - -// ## Handler -handler = async function({ - config, - tools: {log} - }) { - var $status, content, data, err, i, key, len, ref, source; - // for now only support directory type path option - content = {}; - content[config.mount] = {}; - ref = ['mount', 'perm', 'uid', 'gid', 'age', 'argu']; - for (i = 0, len = ref.length; i < len; i++) { - key = ref[i]; - content[config.mount][key] = config[key]; - } - content[config.mount]['type'] = 'd'; - if (config.uid != null) { - if (!/^[0-9]+/.exec(config.uid)) { - if (config.name == null) { - config.name = config.uid; - } - } - } - if (config.target == null) { - config.target = config.name != null ? `/etc/tmpfiles.d/${config.name}.conf` : '/etc/tmpfiles.d/default.conf'; - } - log({ - message: `target set to ${config.target}`, - level: 'DEBUG' - }); - if (config.merge) { - log({ - message: "opening target file for merge", - level: 'DEBUG' - }); - try { - ({data} = (await this.fs.base.readFile({ - target: config.target, - encoding: 'utf8' - }))); - source = utils.tmpfs.parse(data); - content = merge(source, content); - log({ - message: "content has been merged", - level: 'DEBUG' - }); - } catch (error) { - err = error; - if (err.code !== 'NIKITA_FS_CRS_TARGET_ENOENT') { - throw err; - } - } - } - // Serialize and write the content - content = utils.tmpfs.stringify(content); - ({$status} = (await this.file({ - // $debug: true - content: content, - gid: config.gid, - mode: config.mode, - target: config.target, - uid: config.uid - }))); - if ($status) { - log({ - message: `re-creating ${config.mount} tmpfs file`, - level: 'INFO' - }); - await this.execute({ - command: `systemd-tmpfiles --remove ${config.target}` - }); - await this.execute({ - command: `systemd-tmpfiles --create ${config.target}` - }); - } - return void 0; -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - definitions: definitions - } -}; - -// ## Dependencies -utils = require('./utils'); - -({merge} = require('mixme')); - -// [conf-tmpfs]: https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html diff --git a/packages/system/lib/tmpfs/README.md b/packages/system/lib/tmpfs/README.md new file mode 100644 index 000000000..da95daff1 --- /dev/null +++ b/packages/system/lib/tmpfs/README.md @@ -0,0 +1,16 @@ + +# `nikita.system.tmpfs` + +Mount a directory with tmpfs.d as a [tmpfs](https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html) configuration file. + +## Callback parameters + +* `$status` + Wheter the directory was mounted or already mounted. + +# Example + +All parameters can be omitted except type. nikita.tmpfs will ommit by replacing +the undefined value as '-', which does apply the os default behavior. + +Setting uid/gid to '-', make the os creating the target owned by root:root. diff --git a/packages/system/lib/tmpfs/index.js b/packages/system/lib/tmpfs/index.js new file mode 100644 index 000000000..bd345f7fe --- /dev/null +++ b/packages/system/lib/tmpfs/index.js @@ -0,0 +1,77 @@ + +// Dependencies +const {merge} = require('mixme'); +const utils = require('../utils'); +const definitions = require('./schema.json'); + +// Action +module.exports = { + handler: async function ({ config, tools: { log } }) { + // for now only support directory type path option + let content = {}; + content[config.mount] = {}; + for (const key of ["mount", "perm", "uid", "gid", "age", "argu"]) { + content[config.mount][key] = config[key]; + } + content[config.mount]["type"] = "d"; + if (config.uid != null) { + if (!/^[0-9]+/.exec(config.uid)) { + if (config.name == null) { + config.name = config.uid; + } + } + } + if (config.target == null) { + config.target = + config.name != null + ? `/etc/tmpfiles.d/${config.name}.conf` + : "/etc/tmpfiles.d/default.conf"; + } + log({ + message: `target set to ${config.target}`, + level: "DEBUG", + }); + if (config.merge) { + log("DEBUG", "opening target file for merge"); + try { + const { data } = await this.fs.base.readFile({ + target: config.target, + encoding: "utf8", + }); + content = merge(utils.tmpfs.parse(data), content); + log({ + message: "content has been merged", + level: "DEBUG", + }); + } catch (error) { + if (error.code !== "NIKITA_FS_CRS_TARGET_ENOENT") { + throw error; + } + } + } + // Serialize and write the content + content = utils.tmpfs.stringify(content); + const { $status } = await this.file({ + content: content, + gid: config.gid, + mode: config.mode, + target: config.target, + uid: config.uid, + }); + if ($status) { + log({ + message: `re-creating ${config.mount} tmpfs file`, + level: "INFO", + }); + await this.execute({ + command: `systemd-tmpfiles --remove ${config.target}`, + }); + await this.execute({ + command: `systemd-tmpfiles --create ${config.target}`, + }); + } + }, + metadata: { + definitions: definitions, + }, +}; diff --git a/packages/system/lib/tmpfs/schema.json b/packages/system/lib/tmpfs/schema.json new file mode 100644 index 000000000..8e849b299 --- /dev/null +++ b/packages/system/lib/tmpfs/schema.json @@ -0,0 +1,60 @@ +{ + "config": { + "type": "object", + "properties": { + "age": { + "type": "string", + "description": "Used to decide what files to delete when cleaning." + }, + "argument": { + "type": "string", + "description": "The destination path of the symlink if type is `L`." + }, + "backup": { + "type": [ + "boolean", + "string" + ], + "default": true, + "description": "Create a backup, append a provided string to the filename extension or\na timestamp if value is not a string, only apply if the target file\nexists and is modified." + }, + "gid": { + "$ref": "module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid", + "description": "File group name or group id." + }, + "merge": { + "type": "boolean", + "default": true, + "description": "Overrides properties if already exits." + }, + "mount": { + "type": "string", + "description": "The mount point dir to create on system startup." + }, + "mode": { + "$ref": "module://@nikitajs/core/lib/actions/fs/chmod#/definitions/config/properties/mode", + "description": "Mode of the target configuration file" + }, + "name": { + "type": "string", + "description": "The file name, can not be used with target. If only `name` is set, it\nwrites the content to default configuration directory and creates the\nfile as '`name`.conf'." + }, + "perm": { + "type": "string", + "default": "0644", + "description": "Mount path mode in string format like `\"0644\"`." + }, + "target": { + "type": "string", + "description": "File path where to write content to. Defined to\n/etc/tmpfiles.d/{config.uid}.conf if uid is defined or\n/etc/tmpfiles.d/default.conf." + }, + "uid": { + "$ref": "module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid", + "description": "File user name or user id." + } + }, + "required": [ + "mount" + ] + } +} diff --git a/packages/system/lib/uid_gid.js b/packages/system/lib/uid_gid.js deleted file mode 100644 index 1280a3650..000000000 --- a/packages/system/lib/uid_gid.js +++ /dev/null @@ -1,92 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.uid_gid` - -// Normalize the "uid" and "gid" properties. A username defined by the "uid" option will -// be converted to a Unix user ID and a group defined by the "gid" option will -// be converted to a Unix group ID. - -// At the moment, this only work with Unix username because it only read the -// "/etc/passwd" file. A future implementation might execute a system command to -// retrieve information from external identity providers. - -// ## Exemple - -// ```js -// const {uid, gid} = await nikita.system.uid_gid({ -// uid: 'myuser', -// gid: 'mygroup' -// }) -// console.info(`User uid is ${config.uid}`) -// console.info(`Group gid is ${config.gid}`) -// ``` - -// ## Hooks -var definitions, handler, on_action; - -on_action = function({config}) { - if (typeof config.uid === 'string' && /^\d+$/.test(config.uid)) { - config.uid = parseInt(config.uid, 10); - } - if (typeof config.gid === 'string' && /^\d+$/.test(config.gid)) { - return config.gid = parseInt(config.gid, 10); - } -}; - -// ## Schema definitions -definitions = { - config: { - type: 'object', - properties: { - 'gid': { - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid' - }, - 'group_target': { - type: 'string', - description: `Path to the group definition file, default to "/etc/group"` - }, - 'passwd_target': { - type: 'string', - description: `Path to the passwd definition file, default to "/etc/passwd".` - }, - 'uid': { - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid' - } - } - } -}; - -// ## Handler -handler = async function({config}, callback) { - var group, user; - if (config.uid && typeof config.uid === 'string') { - ({user} = (await this.system.user.read({ - target: config.passwd_target, - uid: config.uid - }))); - config.uid = user.uid; - config.default_gid = user.gid; - } - if (config.gid && typeof config.gid === 'string') { - ({group} = (await this.system.group.read({ - target: config.group_target, - gid: config.gid - }))); - config.gid = group.gid; - } - return { - uid: config.uid, - gid: config.gid, - default_gid: config.default_gid - }; -}; - -// ## Exports -module.exports = { - handler: handler, - hooks: { - on_action: on_action - }, - metadata: { - definitions: definitions - } -}; diff --git a/packages/system/lib/uid_gid/README.md b/packages/system/lib/uid_gid/README.md new file mode 100644 index 000000000..c3f19c6db --- /dev/null +++ b/packages/system/lib/uid_gid/README.md @@ -0,0 +1,21 @@ + +# `nikita.system.uid_gid` + +Normalize the "uid" and "gid" properties. A username defined by the "uid" option will +be converted to a Unix user ID and a group defined by the "gid" option will +be converted to a Unix group ID. + +At the moment, this only work with Unix username because it only read the +"/etc/passwd" file. A future implementation might execute a system command to +retrieve information from external identity providers. + +## Exemple + +```js +const {uid, gid} = await nikita.system.uid_gid({ + uid: 'myuser', + gid: 'mygroup' +}) +console.info(`User uid is ${config.uid}`) +console.info(`Group gid is ${config.gid}`) +``` diff --git a/packages/system/lib/uid_gid/index.js b/packages/system/lib/uid_gid/index.js new file mode 100644 index 000000000..d81019625 --- /dev/null +++ b/packages/system/lib/uid_gid/index.js @@ -0,0 +1,41 @@ +// Dependencies +const definitions = require("./schema.json"); + +// Action +module.exports = { + handler: async function ({ config }) { + if (typeof config.uid === "string") { + const { user } = await this.system.user.read({ + target: config.passwd_target, + uid: config.uid, + }); + config.uid = user.uid; + config.default_gid = user.gid; + } + if (typeof config.gid === "string") { + const { group } = await this.system.group.read({ + target: config.group_target, + gid: config.gid, + }); + config.gid = group.gid; + } + return { + uid: config.uid, + gid: config.gid, + default_gid: config.default_gid, + }; + }, + hooks: { + on_action: function ({ config }) { + if (typeof config.uid === "string" && /^\d+$/.test(config.uid)) { + config.uid = parseInt(config.uid, 10); + } + if (typeof config.gid === "string" && /^\d+$/.test(config.gid)) { + config.gid = parseInt(config.gid, 10); + } + }, + }, + metadata: { + definitions: definitions, + }, +}; diff --git a/packages/system/lib/uid_gid/schema.json b/packages/system/lib/uid_gid/schema.json new file mode 100644 index 000000000..2b5194810 --- /dev/null +++ b/packages/system/lib/uid_gid/schema.json @@ -0,0 +1,21 @@ +{ + "config": { + "type": "object", + "properties": { + "gid": { + "$ref": "module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid" + }, + "group_target": { + "type": "string", + "description": "Path to the group definition file, default to \"/etc/group\"" + }, + "passwd_target": { + "type": "string", + "description": "Path to the passwd definition file, default to \"/etc/passwd\"." + }, + "uid": { + "$ref": "module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid" + } + } + } +} diff --git a/packages/system/lib/user/README.md b/packages/system/lib/user/README.md new file mode 100644 index 000000000..1a639e226 --- /dev/null +++ b/packages/system/lib/user/README.md @@ -0,0 +1,32 @@ + +# `nikita.system.user` + +Create or modify a Unix user. + +If the user home is provided, its parent directory will be created with root +ownerships and 0644 permissions unless it already exists. + +## Callback parameters + +* `$status` + Value is "true" if user was created or modified. + +## Example + +```js +const {$status} = await nikita.system.user({ + name: 'a_user', + system: true, + uid: 490, + gid: 10, + comment: 'A System User' +}) +console.log(`User created: ${$status}`) +``` + +The result of the above action can be viewed with the command +`cat /etc/passwd | grep myself` producing an output similar to +"a\_user:x:490:490:A System User:/home/a\_user:/bin/bash". You can also check +you are a member of the "wheel" group (gid of "10") with the command +`id a\_user` producing an output similar to +"uid=490(hive) gid=10(wheel) groups=10(wheel)". diff --git a/packages/system/lib/user/index.js b/packages/system/lib/user/index.js index d589c46a5..d1077f1f4 100644 --- a/packages/system/lib/user/index.js +++ b/packages/system/lib/user/index.js @@ -1,306 +1,149 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.user` -// Create or modify a Unix user. +// Dependencies +const path = require('path'); +const dedent = require('dedent'); +const utils = require("../utils"); +const definitions = require('./schema.json'); -// If the user home is provided, its parent directory will be created with root -// ownerships and 0644 permissions unless it already exists. - -// ## Callback parameters - -// * `$status` -// Value is "true" if user was created or modified. - -// ## Example - -// ```js -// const {$status} = await nikita.system.user({ -// name: 'a_user', -// system: true, -// uid: 490, -// gid: 10, -// comment: 'A System User' -// }) -// console.log(`User created: ${$status}`) -// ``` - -// The result of the above action can be viewed with the command -// `cat /etc/passwd | grep myself` producing an output similar to -// "a\_user:x:490:490:A System User:/home/a\_user:/bin/bash". You can also check -// you are a member of the "wheel" group (gid of "10") with the command -// `id a\_user` producing an output similar to -// "uid=490(hive) gid=10(wheel) groups=10(wheel)". - -// ## Hooks -var definitions, handler, on_action, path, utils; - -on_action = function({config}) { - switch (config.shell) { - case true: - config.shell = '/bin/sh'; - break; - case false: - config.shell = '/sbin/nologin'; - } - if (typeof config.groups === 'string') { - return config.groups = config.groups.split(','); - } -}; - -// ## Schema definitions -definitions = { - config: { - type: 'object', - properties: { - 'comment': { - type: 'string', - description: `Short description of the login.` - }, - 'expiredate': { - type: 'integer', - description: `The date on which the user account is disabled.` - }, - 'gid': { - type: 'integer', - description: `Group name or number of the user´s initial login group.` - }, - 'groups': { - type: 'array', - items: { - type: 'string' - }, - description: `List of supplementary groups which the user is also a member of.` - }, - 'home': { - type: 'string', - description: `Value for the user´s login directory, default to the login name -appended to "BASE_DIR".` - }, - 'inactive': { - type: 'integer', - description: `The number of days after a password has expired before the account -will be disabled.` - }, - 'name': { - type: 'string', - description: `Login name of the user.` - }, - 'no_home_ownership': { - type: 'boolean', - description: `Disable ownership on home directory which default to the "uid" and -"gid" config, default is "false".` - }, - 'password': { - type: 'string', - description: `The unencrypted password.` - }, - 'password_sync': { - type: 'boolean', - default: true, - description: `Synchronize password` - }, - 'shell': { - // oneOf: [ - // type: 'boolean' - // , - // type: 'string' - // ] - type: ['boolean', 'string'], - default: '/bin/sh', - description: `Path to the user shell, set to "/sbin/nologin" if \`false\` and "/bin/sh" -if \`true\` or \`undefined\`.` - }, - 'skel': { - type: 'string', - description: `The skeleton directory, which contains files and directories to be -copied in the user´s home directory, when the home directory is -created by useradd.` - }, - 'system': { - type: 'boolean', - description: `Create a system account, such user are not created with a home by -default, set the "home" option if we it to be created.` - }, - 'uid': { - type: 'integer', - description: `Numerical value of the user´s ID, must not exist.` - } - }, - required: ['name'] - } -}; - -// ## Handler -handler = async function({ - metadata, +// Action +module.exports = { + handler: async function({ config, tools: {log} }) { - var $status, changed, err, group, groups, groups_info, i, j, k, len, len1, ref, ref1, user_info, users; - log({ - message: "Entering user", - level: 'DEBUG' - }); - if (config.system == null) { - config.system = false; - } - if (config.gid == null) { - config.gid = null; - } - if (config.password_sync == null) { - config.password_sync = true; - } - if (typeof config.shell === "function" ? config.shell(typeof config.shell !== 'string') : void 0) { - throw Error(`Invalid option 'shell': ${JSON.strinfigy(config.shell)}`); - } - user_info = groups_info = null; - ({users} = (await this.system.user.read())); - user_info = users[config.name]; - log(user_info ? { - message: `Got user information for ${JSON.stringify(config.name)}`, - level: 'DEBUG', - module: 'nikita/lib/system/group' - } : { - message: `User ${JSON.stringify(config.name)} not present`, - level: 'DEBUG', - module: 'nikita/lib/system/group' - }); - // Get group information if - // * user already exists - // * we need to compare groups membership - ({groups} = (await this.system.group.read({ - $if: user_info && config.groups - }))); - groups_info = groups; - if (groups_info) { - log({ - message: `Got group information for ${JSON.stringify(config.name)}`, - level: 'DEBUG' - }); - } - if (config.home) { - this.fs.mkdir({ - $unless_exists: path.dirname(config.home), - target: path.dirname(config.home), - uid: 0, - gid: 0, - mode: 0o0644 // Same as '/home' - }); - } - if (!user_info) { - await this.execute([ - { - code: [0, - 9], - command: ['useradd', - config.system ? '-r' : void 0, - !config.home ? '-M' : void 0, - config.home ? '-m' : void 0, - config.home ? `-d ${config.home}` : void 0, - config.shell ? `-s ${config.shell}` : void 0, - config.comment ? `-c ${utils.string.escapeshellarg(config.comment)}` : void 0, - config.uid ? `-u ${config.uid}` : void 0, - config.gid ? `-g ${config.gid}` : void 0, - config.expiredate ? `-e ${config.expiredate}` : void 0, - config.inactive ? `-f ${config.inactive}` : void 0, - config.groups ? `-G ${config.groups.join(',')}` : void 0, - config.skel ? `-k ${config.skel}` : void 0, - `${config.name}`].join(' ') - }, - { - $if: config.home, - command: `chown ${config.name}. ${config.home}` - } - ]); - log({ - message: "User defined elsewhere than '/etc/passwd', exit code is 9", - level: 'WARN' + log('DEBUG', 'Entering user'); + if (typeof config.shell === "function" ? config.shell(typeof config.shell !== 'string') : void 0) { + throw Error(`Invalid option 'shell': ${JSON.strinfigy(config.shell)}`); + } + const {users} = await this.system.user.read(); + const user_info = users[config.name]; + log( + "DEBUG", + user_info + ? `Got user information for ${JSON.stringify(config.name)}` + : `User ${JSON.stringify(config.name)} not present` + ); + // Get group information if + // * user already exists + // * we need to compare groups membership + const {groups: groups_info} = await this.system.group.read({ + $if: user_info && config.groups }); - } else { - changed = []; - ref = ['uid', 'home', 'shell', 'comment', 'gid']; - for (i = 0, len = ref.length; i < len; i++) { - k = ref[i]; - if ((config[k] != null) && user_info[k] !== config[k]) { - changed.push(k); - } + if (groups_info) { + log('DEBUG', `Got group information for ${JSON.stringify(config.name)}`); + } + if (config.home) { + await this.fs.mkdir({ + $unless_exists: path.dirname(config.home), + target: path.dirname(config.home), + uid: 0, + gid: 0, + mode: 0o0644 // Same as '/home' + }); } - if (config.groups) { - ref1 = config.groups; - for (j = 0, len1 = ref1.length; j < len1; j++) { - group = ref1[j]; - if (!groups_info[group]) { - throw Error(`Group does not exist: ${group}`); + if (!user_info) { + await this.execute([ + { + code: [0, 9], + command: [ + "useradd", + config.system && "-r", + !config.home && "-M", + config.home && "-m", + config.home && `-d ${config.home}`, + config.shell && `-s ${config.shell}`, + config.comment && `-c ${utils.string.escapeshellarg(config.comment)}`, + config.uid && `-u ${config.uid}`, + config.gid && `-g ${config.gid}`, + config.expiredate && `-e ${config.expiredate}`, + config.inactive && `-f ${config.inactive}`, + config.groups && `-G ${config.groups.join(",")}`, + config.skel && `-k ${config.skel}`, + `${config.name}`, + ].filter(Boolean).join(" "), + }, + { + $if: config.home, + command: `chown ${config.name}. ${config.home}`, + }, + ]); + log("WARN", "User defined elsewhere than '/etc/passwd', exit code is 9"); + } else { + const changed = []; + for (const k of ['uid', 'home', 'shell', 'comment', 'gid']) { + if ((config[k] != null) && user_info[k] !== config[k]) { + changed.push(k); } - if (groups_info[group].users.indexOf(config.name) === -1) { - changed.push('groups'); + } + if (config.groups) { + for (const group of config.groups) { + if (!groups_info[group]) { + throw Error(`Group does not exist: ${group}`); + } + if (groups_info[group].users.indexOf(config.name) === -1) { + changed.push('groups'); + } } } - } - log(changed.length ? { - message: `User ${config.name} modified`, - level: 'WARN', - module: 'nikita/lib/system/user/add' - } : { - message: `User ${config.name} not modified`, - level: 'DEBUG', - module: 'nikita/lib/system/user/add' - }); - try { - await this.execute({ - $if: changed.length, - command: ['usermod', config.home ? `-d ${config.home}` : void 0, config.shell ? `-s ${config.shell}` : void 0, config.comment != null ? `-c ${utils.string.escapeshellarg(config.comment)}` : void 0, config.gid ? `-g ${config.gid}` : void 0, config.groups ? `-G ${config.groups.join(',')}` : void 0, config.uid ? `-u ${config.uid}` : void 0, `${config.name}`].join(' ') + log(changed.length ? { + message: `User ${config.name} modified`, + level: 'WARN' + } : { + message: `User ${config.name} not modified`, + level: 'DEBUG' }); - } catch (error) { - err = error; - if (err.exit_code === 8) { - throw Error(`User ${config.name} is logged in`); - } else { - throw err; + try { + await this.execute({ + $if: changed.length, + command: ['usermod', config.home ? `-d ${config.home}` : void 0, config.shell ? `-s ${config.shell}` : void 0, config.comment != null ? `-c ${utils.string.escapeshellarg(config.comment)}` : void 0, config.gid ? `-g ${config.gid}` : void 0, config.groups ? `-G ${config.groups.join(',')}` : void 0, config.uid ? `-u ${config.uid}` : void 0, `${config.name}`].join(' ') + }); + } catch (error) { + if (error.exit_code === 8) { + throw Error(`User ${config.name} is logged in`); + } else { + throw error; + } + } + if (config.home && (config.uid || config.gid)) { + await this.fs.chown({ + $if_exists: config.home, + $unless: config.no_home_ownership, + target: config.home, + uid: config.uid, + gid: config.gid + }); } } - if (config.home && (config.uid || config.gid)) { - await this.fs.chown({ - $if_exists: config.home, - $unless: config.no_home_ownership, - target: config.home, - uid: config.uid, - gid: config.gid - }); - } - } - // TODO, detect changes in password - // echo #{config.password} | passwd --stdin #{config.name} - if (config.password_sync && config.password) { - ({$status} = (await this.execute({ - command: `hash=$(echo ${config.password} | openssl passwd -1 -stdin) -usermod --pass="$hash" ${config.name}` - }))); - if ($status) { - // arch_chroot: config.arch_chroot - // rootdir: config.rootdir - // sudo: config.sudo - return log({ - message: "Password modified", - level: 'WARN' + // TODO, detect changes in password + // echo #{config.password} | passwd --stdin #{config.name} + if (config.password_sync && config.password) { + const {$status} = await this.execute({ + command: dedent` + hash=$(echo ${config.password} | openssl passwd -1 -stdin) + usermod --pass="$hash" ${config.name} + ` }); + if ($status) { + return log('WARN', "Password modified"); + } } - } -}; - -// ## Exports -module.exports = { - handler: handler, + }, hooks: { - on_action: on_action + on_action: function({config}) { + switch (config.shell) { + case true: + config.shell = '/bin/sh'; + break; + case false: + config.shell = '/sbin/nologin'; + } + if (typeof config.groups === 'string') { + config.groups = config.groups.split(','); + } + } }, metadata: { argument_to_config: 'name', definitions: definitions } }; - -// ## Dependencies -path = require('path'); - -utils = require('../utils'); diff --git a/packages/system/lib/user/read.js b/packages/system/lib/user/read.js deleted file mode 100644 index 1212e601d..000000000 --- a/packages/system/lib/user/read.js +++ /dev/null @@ -1,148 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.user.read` - -// Read and parse the passwd definition file located in "/etc/passwd". - -// ## Output parameters - -// * `users` -// An object where keys are the usernames and values are the user properties. -// See the parameter `user` for a list of available properties. -// * `user` -// Properties associated witht the user, only if the input parameter `uid` is -// provided. Available properties are: -// * `user` (string) -// Username. -// * `uid` (integer) -// User Id. -// * `comment` (string) -// User description -// * `home` (string) -// User home directory. -// * `shell` (string) -// Default user shell command. - -// ## Example - -// ```js -// const {$status, users} = await nikita -// .file({ -// target: "/tmp/etc/passwd", -// content: "root:x:0:0:root:/root:/bin/bash" -// }) -// .system.user.read({ -// target: "/tmp/etc/passwd" -// }) -// assert.equal($status, false) -// assert.deepEqual(users, { -// "root": { user: 'root', uid: 0, gid: 0, comment: 'root', home: '/root', shell: '/bin/bash' } -// }) -// ``` - -// ## Implementation - -// The default implementation use the `getent passwd` command. It is possible to -// read an alternative `/etc/passwd` file by setting the `target` option to the -// targeted file. - -// ## Schema definitions -var definitions, handler, utils; - -definitions = { - config: { - type: 'object', - properties: { - 'target': { - type: 'string', - description: `Path to the passwd definition file, use the \`getent passwd\` command by -default which use to "/etc/passwd".` - }, - 'uid': { - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid', - description: `Retrieve the information for a specific username or uid.` - } - } - } -}; - -// ## Handler -handler = async function({config}) { - var data, passwd, stdout, str2passwd, user; - if (typeof config.uid === 'string' && /\d+/.test(config.uid)) { - config.uid = parseInt(config.uid, 10); - } - // Parse the passwd output - str2passwd = function(data) { - var i, len, line, passwd, ref; - passwd = {}; - ref = utils.string.lines(data); - for (i = 0, len = ref.length; i < len; i++) { - line = ref[i]; - line = /(.*)\:\w\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)/.exec(line); - if (!line) { - continue; - } - passwd[line[1]] = { - user: line[1], - uid: parseInt(line[2]), - gid: parseInt(line[3]), - comment: line[4], - home: line[5], - shell: line[6] - }; - } - return passwd; - }; - // Fetch the users information - if (!config.target) { - ({stdout} = (await this.execute({ - command: 'getent passwd' - }))); - passwd = str2passwd(stdout); - } else { - ({data} = (await this.fs.base.readFile({ - target: config.target, - encoding: 'ascii' - }))); - passwd = str2passwd(data); - } - if (!config.uid) { - return { - // Return all the users - users: passwd - }; - } - // Return a user by username - if (typeof config.uid === 'string') { - user = passwd[config.uid]; - if (!user) { - throw Error(`Invalid Option: no uid matching ${JSON.stringify(config.uid)}`); - } - return { - user: user - }; - } else { - // Return a user by uid - user = Object.values(passwd).filter(function(user) { - return user.uid === config.uid; - })[0]; - if (!user) { - throw Error(`Invalid Option: no uid matching ${JSON.stringify(config.uid)}`); - } - return { - user: user - }; - } -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - definitions: definitions, - shy: true - } -}; - -// ## Dependencies -utils = require('../utils'); diff --git a/packages/system/lib/user/read/README.md b/packages/system/lib/user/read/README.md new file mode 100644 index 000000000..049ef12dd --- /dev/null +++ b/packages/system/lib/user/read/README.md @@ -0,0 +1,46 @@ + +# `nikita.system.user.read` + +Read and parse the passwd definition file located in "/etc/passwd". + +## Output parameters + +* `users` + An object where keys are the usernames and values are the user properties. + See the parameter `user` for a list of available properties. +* `user` + Properties associated witht the user, only if the input parameter `uid` is + provided. Available properties are: + * `user` (string) + Username. + * `uid` (integer) + User Id. + * `comment` (string) + User description + * `home` (string) + User home directory. + * `shell` (string) + Default user shell command. + +## Example + +```js +const {$status, users} = await nikita +.file({ + target: "/tmp/etc/passwd", + content: "root:x:0:0:root:/root:/bin/bash" +}) +.system.user.read({ + target: "/tmp/etc/passwd" +}) +assert.equal($status, false) +assert.deepEqual(users, { + "root": { user: 'root', uid: 0, gid: 0, comment: 'root', home: '/root', shell: '/bin/bash' } +}) +``` + +## Implementation + +The default implementation use the `getent passwd` command. It is possible to +read an alternative `/etc/passwd` file by setting the `target` option to the +targeted file. diff --git a/packages/system/lib/user/read/index.js b/packages/system/lib/user/read/index.js new file mode 100644 index 000000000..eb00cb760 --- /dev/null +++ b/packages/system/lib/user/read/index.js @@ -0,0 +1,81 @@ +// Dependencies +const utils = require("../../utils"); +const definitions = require("./schema.json"); + +// Parse the passwd output +const str2passwd = function (data) { + const passwd = {}; + for (const line of utils.string.lines(data)) { + const record = /(.*)\:\w\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)/.exec(line); + if (!record) { + continue; + } + passwd[record[1]] = { + user: record[1], + uid: parseInt(record[2]), + gid: parseInt(record[3]), + comment: record[4], + home: record[5], + shell: record[6], + }; + } + return passwd; +}; + +// Action +module.exports = { + handler: async function ({ config }) { + if (typeof config.uid === "string" && /\d+/.test(config.uid)) { + config.uid = parseInt(config.uid, 10); + } + // Fetch the users information + let passwd; + if (config.target) { + ({ data: passwd } = await this.fs.base.readFile({ + target: config.target, + encoding: "ascii", + format: ({ data }) => str2passwd(data), + })); + } else { + ({ data: passwd } = await this.execute({ + command: "getent passwd", + format: ({ stdout }) => str2passwd(stdout), + })); + } + if (!config.uid) { + return { + // Return all the users + users: passwd, + }; + } + // Return a user by username + if (typeof config.uid === "string") { + const user = passwd[config.uid]; + if (!user) { + throw Error( + `Invalid Option: no uid matching ${JSON.stringify(config.uid)}` + ); + } + return { + user: user, + }; + } else { + // Return a user by uid + const user = Object.values(passwd).filter(function (user) { + return user.uid === config.uid; + })[0]; + if (!user) { + throw Error( + `Invalid Option: no uid matching ${JSON.stringify(config.uid)}` + ); + } + return { + user: user, + }; + } + }, + metadata: { + definitions: definitions, + shy: true, + }, +}; diff --git a/packages/system/lib/user/read/schema.json b/packages/system/lib/user/read/schema.json new file mode 100644 index 000000000..3053a87ab --- /dev/null +++ b/packages/system/lib/user/read/schema.json @@ -0,0 +1,15 @@ +{ + "config": { + "type": "object", + "properties": { + "target": { + "type": "string", + "description": "Path to the passwd definition file, use the `getent passwd` command by\ndefault which use to \"/etc/passwd\"." + }, + "uid": { + "$ref": "module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid", + "description": "Retrieve the information for a specific username or uid." + } + } + } +} diff --git a/packages/system/lib/user/remove.js b/packages/system/lib/user/remove.js deleted file mode 100644 index cd5d721a4..000000000 --- a/packages/system/lib/user/remove.js +++ /dev/null @@ -1,51 +0,0 @@ -// Generated by CoffeeScript 2.7.0 -// # `nikita.system.user.remove` - -// Create or modify a Unix user. - -// ## Callback parameters - -// * `$status` -// Value is "true" if user was created or modified. - -// ## Example - -// ```js -// const {$status} = await nikita.system.user.remove({ -// name: 'a_user' -// }) -// console.log(`User removed: ${$status}`) -// ``` - -// ## Schema definitions -var definitions, handler; - -definitions = { - config: { - type: 'object', - properties: { - name: { - type: 'string', - description: `Name of the user to removed.` - } - }, - required: ['name'] - } -}; - -// ## Handler -handler = function({config}) { - return this.execute({ - command: `userdel ${config.name}`, - code: [0, 6] - }); -}; - -// ## Exports -module.exports = { - handler: handler, - metadata: { - argument_to_config: 'name' - }, - definitions: definitions -}; diff --git a/packages/system/lib/user/remove/README.md b/packages/system/lib/user/remove/README.md new file mode 100644 index 000000000..e4101485b --- /dev/null +++ b/packages/system/lib/user/remove/README.md @@ -0,0 +1,18 @@ + +# `nikita.system.user.remove` + +Create or modify a Unix user. + +## Callback parameters + +* `$status` + Value is "true" if user was created or modified. + +## Example + +```js +const {$status} = await nikita.system.user.remove({ + name: 'a_user' +}) +console.log(`User removed: ${$status}`) +``` diff --git a/packages/system/lib/user/remove/index.js b/packages/system/lib/user/remove/index.js new file mode 100644 index 000000000..cefc9cb1e --- /dev/null +++ b/packages/system/lib/user/remove/index.js @@ -0,0 +1,17 @@ + +// Dependencies +const definitions = require('./schema.json'); + +// Action +module.exports = { + handler: function({config}) { + this.execute({ + command: `userdel ${config.name}`, + code: [0, 6] + }); + }, + metadata: { + argument_to_config: 'name' + }, + definitions: definitions +}; diff --git a/packages/system/lib/user/remove/schema.json b/packages/system/lib/user/remove/schema.json new file mode 100644 index 000000000..7546a1d62 --- /dev/null +++ b/packages/system/lib/user/remove/schema.json @@ -0,0 +1,14 @@ +{ + "config": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the user to removed." + } + }, + "required": [ + "name" + ] + } +} diff --git a/packages/system/lib/user/schema.json b/packages/system/lib/user/schema.json new file mode 100644 index 000000000..4afebe46a --- /dev/null +++ b/packages/system/lib/user/schema.json @@ -0,0 +1,75 @@ +{ + "config": { + "type": "object", + "properties": { + "comment": { + "type": "string", + "description": "Short description of the login." + }, + "expiredate": { + "type": "integer", + "description": "The date on which the user account is disabled." + }, + "gid": { + "type": "integer", + "description": "Group name or number of the user´s initial login group." + }, + "groups": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of supplementary groups which the user is also a member of." + }, + "home": { + "type": "string", + "description": "Value for the user´s login directory, default to the login name\nappended to \"BASE_DIR\"." + }, + "inactive": { + "type": "integer", + "description": "The number of days after a password has expired before the account\nwill be disabled." + }, + "name": { + "type": "string", + "description": "Login name of the user." + }, + "no_home_ownership": { + "type": "boolean", + "description": "Disable ownership on home directory which default to the \"uid\" and\n\"gid\" config, default is \"false\"." + }, + "password": { + "type": "string", + "description": "The unencrypted password." + }, + "password_sync": { + "type": "boolean", + "default": true, + "description": "Synchronize password" + }, + "shell": { + "type": [ + "boolean", + "string" + ], + "default": "/bin/sh", + "description": "Path to the user shell, set to \"/sbin/nologin\" if `false` and \"/bin/sh\"\nif `true` or `undefined`." + }, + "skel": { + "type": "string", + "description": "The skeleton directory, which contains files and directories to be\ncopied in the user´s home directory, when the home directory is\ncreated by useradd." + }, + "system": { + "type": "boolean", + "default": false, + "description": "Create a system account, such user are not created with a home by\ndefault, set the \"home\" option if we it to be created." + }, + "uid": { + "type": "integer", + "description": "Numerical value of the user´s ID, must not exist." + } + }, + "required": [ + "name" + ] + } +} diff --git a/packages/system/lib/utils/cgconfig.js b/packages/system/lib/utils/cgconfig.js index 8d0394c10..01b7c4394 100644 --- a/packages/system/lib/utils/cgconfig.js +++ b/packages/system/lib/utils/cgconfig.js @@ -5,25 +5,23 @@ utils = require('@nikitajs/core/lib/utils'); module.exports = { parse: function(str) { - var current_controller_name, current_default, current_group, current_group_controller, current_group_name, current_group_perm, current_group_perm_content, current_group_section, current_group_section_perm_name, current_mount, current_mount_section, lines, list_of_group_sections, list_of_mount_sections; - lines = utils.string.lines(str); - list_of_mount_sections = []; - list_of_group_sections = {}; + let list_of_mount_sections = []; + let list_of_group_sections = {}; // variable which hold the cursor position - current_mount = false; - current_group = false; - current_group_name = ''; - current_group_controller = false; - current_group_perm = false; - current_group_perm_content = false; - current_default = false; + let current_mount = false; + let current_group = false; + let current_group_name = ''; + let current_group_controller = false; + let current_group_perm = false; + let current_group_perm_content = false; + let current_default = false; // variables which hold the data - current_mount_section = null; - current_group_section = null; // group section is a tree but only of group - current_controller_name = null; - current_group_section_perm_name = null; - lines.forEach(function(line, _, __) { - var base, base1, match, match_admin, name, name1, name2, name3, name4, sep, type, value; + let current_mount_section = null; + let current_group_section = null; // group section is a tree but only of group + let current_controller_name = null; + let current_group_section_perm_name = null; + utils.string.lines(str).forEach(function(line, _, __) { + var base, base1, match, match_admin, name, name1, name2, name4, sep, type, value; if (!line || line.match(/^\s*$/)) { return; } @@ -104,7 +102,7 @@ module.exports = { current_group_controller = true; current_controller_name = match[1]; current_group_section[`${current_controller_name}`] = {}; - return (base = list_of_group_sections[`${current_group_name}`])[name3 = `${current_controller_name}`] != null ? base[name3] : base[name3] = {}; + return (base = list_of_group_sections[`${current_group_name}`])[current_controller_name] != null ? base[current_controller_name] : base[current_controller_name] = {}; } } else if (current_group_perm && current_group_perm_content) { // perm config line = line.replace(';', ''); @@ -141,14 +139,13 @@ module.exports = { }; }, stringify: function(obj, config = {}) { - var count, group, group_render, i, indent, j, k, key, l, len, mount, mount_render, name, prop, ref, ref1, ref2, ref3, ref4, render, sections, val, value; + var i, indent, j, k, l, len, mount, name, ref, ref1, val; if (obj.mounts == null) { obj.mounts = []; } if (obj.groups == null) { obj.groups = {}; } - render = ""; if (config.indent == null) { config.indent = 2; } @@ -156,9 +153,9 @@ module.exports = { for (i = j = 1, ref = config.indent; (1 <= ref ? j <= ref : j >= ref); i = 1 <= ref ? ++j : --j) { indent += ' '; } - sections = []; + const sections = []; if (obj.mounts.length !== 0) { - mount_render = "mount {\n"; + let mount_render = "mount {\n"; ref1 = obj.mounts; for (k = l = 0, len = ref1.length; l < len; k = ++l) { mount = ref1[k]; @@ -167,29 +164,26 @@ module.exports = { mount_render += '}'; sections.push(mount_render); } - count = 0; - ref2 = obj.groups; - for (name in ref2) { - group = ref2[name]; - group_render = (name === '') || (name === 'default') ? 'default {\n' : `group ${name} {\n`; - for (key in group) { - value = group[key]; + let count = 0; + for (name in obj.groups) { + const group = obj.groups[name]; + let group_render = (name === '') || (name === 'default') ? 'default {\n' : `group ${name} {\n`; + for (const key in group) { + const value = group[key]; if (key === 'perm') { group_render += `${indent}perm {\n`; if (value['admin'] != null) { group_render += `${indent}${indent}admin {\n`; - ref3 = value['admin']; - for (prop in ref3) { - val = ref3[prop]; + for (const prop in value['admin']) { + val = value['admin'][prop]; group_render += `${indent}${indent}${indent}${prop} = ${val};\n`; } group_render += `${indent}${indent}}\n`; } if (value['task'] != null) { group_render += `${indent}${indent}task {\n`; - ref4 = value['task']; - for (prop in ref4) { - val = ref4[prop]; + for (const prop in value['task']) { + const val = value['task'][prop]; group_render += `${indent}${indent}${indent}${prop} = ${val};\n`; } group_render += `${indent}${indent}}\n`; @@ -197,8 +191,8 @@ module.exports = { group_render += `${indent}}\n`; } else { group_render += `${indent}${key} {\n`; - for (prop in value) { - val = value[prop]; + for (const prop in value) { + const val = value[prop]; group_render += `${indent}${indent}${prop} = ${val};\n`; } group_render += `${indent}}\n`; diff --git a/packages/system/package.json b/packages/system/package.json index bb6969815..637901e5e 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -42,7 +42,8 @@ ], "dependencies": { "@nikitajs/file": "^1.0.0-alpha.3", - "diff": "^5.1.0" + "diff": "^5.1.0", + "dedent": "^1.2.0" }, "devDependencies": { "@nikitajs/lxd-runner": "^1.0.0-alpha.0", @@ -65,7 +66,7 @@ "require": [ "should", "coffeescript/register", - "@nikitajs/system/src/register" + "@nikitajs/system/lib/register" ], "inline-diffs": true, "timeout": 20000, @@ -83,7 +84,6 @@ "directory": "packages/system" }, "scripts": { - "build": "coffee -b -o lib src && find lib -type f | xargs sed -i -e 's/@nikitajs\\/system\\/src/@nikitajs\\/system\\/lib/g'", "test": "npm run test:local && npm run test:env", "test:env": "env/run.sh", "test:local": "mocha 'test/**/*.coffee'" diff --git a/packages/system/src/cgroups.coffee.md b/packages/system/src/cgroups.coffee.md deleted file mode 100644 index 2f778a937..000000000 --- a/packages/system/src/cgroups.coffee.md +++ /dev/null @@ -1,247 +0,0 @@ - -# `nikita.system.cgroups` - -Nikita action to manipulate cgroups. [cgconfig.conf(5)] describes the -configuration file used by libcgroup to define control groups, their parameters -and also mount points. The configuration file is identical on Ubuntu, RedHat -and CentOS. - -## Implementation - -When reading the current config, nikita uses `cgsnapshot` command in order to -have a well formatted file. It is available on CentOS with the `libcgroup-tools` -package. - -If docker is installed and started, informations about live containers could be -printed, that's why all path under docker/* are ignored. - -## Example - -Example of a group object: - -``` -bibi: - perm: - admin: - uid: 'bibi' - gid: 'bibi' - task: - uid: 'bibi' - gid: 'bibi' - controllers: - cpu: - 'cpu.rt_period_us': '"1000000"' - 'cpu.rt_runtime_us': '"0"' - 'cpu.cfs_period_us': '"100000"' -``` - -Which will result in a file: - -```text -group bibi { - perm { - admin { - uid = bibi; - gid = bibi; - } - task { - uid = bibi; - gid = bibi; - } - } - cpu { - cpu.rt_period_us = "1000000"; - cpu.rt_runtime_us = "0"; - cpu.cfs_period_us = "100000"; - } -} -``` - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'default': - $ref: '#/definitions/group' - description: ''' - The default object of cgconfig file. - ''' - 'groups': - type: 'object' - description: ''' - Object of cgroups to add to cgconfig file. The keys are the - cgroup name, and the values are the cgroup configuration. - ''' - patternProperties: - '.*': # cgroup name - $ref: '#/definitions/group' - additionalProperties: false - 'ignore': - type: 'array' - items: type: 'string' - description: ''' - List of group path to ignore. Only used when merging. - ''' - 'mounts': - type: 'array' - description: ''' - List of mount object to add to cgconfig file. - ''' - 'merge': - type: 'boolean' - default: true - description: ''' - Default to true. Read the config from cgsnapshot command and merge - mounts part of the cgroups. - ''' - 'target': - type: 'string' - description: ''' - The cgconfig configuration file. By default nikita detects provider - based on os. - ''' - anyOf: [ - required: ['groups'] - , - required: ['mounts'] - , - required: ['default'] - ] - group: - type: 'object' - description: ''' - Controllers in the cgroup where the keys represent the name of the - controler. - ''' - properties: - perm: - type: 'object' - description: ''' - Object to describe the taks and limits permissions. - ''' - properties: - 'admin': - $ref: '#/definitions/group_perm' - description: ''' - Who can manage limits - ''' - 'task': - $ref: '#/definitions/group_perm' - description: ''' - Who can add tasks to this group - ''' - cpuset: $ref: '#/definitions/group_controller' - cpu: $ref: '#/definitions/group_controller' - cpuacct: $ref: '#/definitions/group_controller' - blkio: $ref: '#/definitions/group_controller' - memory: $ref: '#/definitions/group_controller' - devices: $ref: '#/definitions/group_controller' - freezer: $ref: '#/definitions/group_controller' - net_cls: $ref: '#/definitions/group_controller' - perf_event: $ref: '#/definitions/group_controller' - net_prio: $ref: '#/definitions/group_controller' - hugetlb: $ref: '#/definitions/group_controller' - pids: $ref: '#/definitions/group_controller' - rdma: $ref: '#/definitions/group_controller' - group_perm: - type: 'object' - properties: - 'uid': oneOf: [ - type: 'integer' - , - type: 'string' - ] - 'gid': oneOf: [ - type: 'integer' - , - type: 'string' - ] - group_controller: - type: 'object' - patternProperties: - '.*': oneOf: [ - type: 'integer' - , - type: 'string' - ] - additionalProperties: false - -## Handler - - handler = ({config}) -> - # throw Error 'Missing cgroups content' unless config.groups? or config.mounts? or config.default? - config.mounts ?= [] - config.groups ?= {} - config.merge ?= true - config.cgconfig = {} - config.cgconfig['mounts'] = config.mounts - config.cgconfig['groups'] = config.groups - config.cgconfig['groups'][''] = config.default if config.default? - config.ignore ?= [] - config.ignore = [config.ignore] unless Array.isArray config.ignore - # Detect Os and version - {os} = await @system.info.os() - # configure parameters based on previous OS dection - store = {} - # Enable cgroup for all distribution, it was restricted to rhel systems - # if ['redhat','centos'].includes os.distribution - if true - {stdout} = await @execute - $shy: true - command: 'cgsnapshot -s 2>&1' - cgconfig = utils.cgconfig.parse stdout - cgconfig.mounts ?= [] - cpus = cgconfig.mounts.filter (mount) -> - mount.type is 'cpu' - cpuaccts = cgconfig.mounts.filter (mount) -> - mount.type is 'cpuacct' - # We choose a path which is mounted by default - # if not @store['nikita:cgroups:cpu_path']? - if cpus.length > 0 - store.cpu_path = cpus[0]['path'].split(',')[0] - # @store['nikita:cgroups:cpu_path'] ?= cpu_path - # a arbitrary path is given based on the - else - switch os.distribution - when 'redhat', 'centos' - majorVersion = os.version.split('.')[0] - switch majorVersion - when '6' - store.cpu_path = '/cgroups/cpu' - when '7' - store.cpu_path = '/sys/fs/cgroup/cpu' - else - throw Error "Nikita does not support cgroups for your RedHat or CentOS version}" - else throw Error "Nikita does not support cgroups on your OS #{os.distribution}" - store.mount = "#{path.posix.dirname store.cpu_path}" - # Running docker containers are remove from cgsnapshot output - if config.merge - groups = {} - for name, group of cgconfig.groups - groups[name] = group unless (name.indexOf('docker/') isnt -1) or (name in config.ignore) - config.cgconfig.groups = merge groups, config.groups - config.cgconfig.mounts.push cgconfig.mounts... - # Write the configuration - config.target ?= '/etc/cgconfig.conf' if ['redhat', 'centos'].includes os.distribution - @file config, - content: utils.cgconfig.stringify config.cgconfig - cgroups: - cpu_path: store.cpu_path - mount: store.mount - -## Exports - - module.exports = - handler: handler - metadata: - definitions: definitions - -## Dependencies - - utils = require './utils' - {merge} = require 'mixme' - path = require 'path' - -[cgconfig.conf(5)]: https://linux.die.net/man/5/cgconfig.conf diff --git a/packages/system/src/group/index.coffee.md b/packages/system/src/group/index.coffee.md deleted file mode 100644 index e36ddf902..000000000 --- a/packages/system/src/group/index.coffee.md +++ /dev/null @@ -1,98 +0,0 @@ - -# `nikita.system.group` - -Create or modify a Unix group. - -## Callback Parameters - -* `$status` - Value is "true" if group was created or modified. - -## Example - -```js -const {$status} = await nikita.system.group({ - name: 'myself' - system: true - gid: 490 -}); -console.log(`Group was created/modified: ${$status}`); -``` - -The result of the above action can be viewed with the command -`cat /etc/group | grep myself` producing an output similar to -"myself:x:490:". - -## Hooks - - on_action = ({config}) -> - config.gid = parseInt config.gid, 10 if typeof config.gid is 'string' - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'gid': - type: 'integer' - description: ''' - Group name or number of the user´s initial login group. - ''' - 'name': - type: 'string' - description: ''' - Login name of the group. - ''' - 'system': - type: 'boolean' - default: false - description: ''' - Create a system account, such user are not created with ahome by - default, set the "home" option if we it to be created. - ''' - required: ['name'] - -## Handler - - handler = ({config, tools: {log}}) -> - config.system ?= false - config.gid ?= null - # throw Error 'Invalid gid option' if config.gid? and isNaN config.gid - {groups} = await @system.group.read() - info = groups[config.name] - log if info - then message: "Got group information for #{JSON.stringify config.name}", level: 'DEBUG', module: 'nikita/lib/system/group' - else message: "Group #{JSON.stringify config.name} not present", level: 'DEBUG', module: 'nikita/lib/system/group' - unless info # Create group - {$status} = await @execute - command: [ - 'groupadd' - '-r' if config.system - "-g #{config.gid}" if config.gid? - config.name - ].join ' ' - code: [0, 9] - log message: "Group defined elsewhere than '/etc/group', exit code is 9", level: 'WARN' unless $status - else # Modify group - changes = ['gid'].filter (k) -> config[k]? and "#{info[k]}" isnt "#{config[k]}" - if changes.length - await @execute - command: [ - 'groupmod' - " -g #{config.gid}" if config.gid - config.name - ].join ' ' - log message: "Group information modified", level: 'WARN' - else - log message: "Group information unchanged", level: 'INFO' - -## Exports - - module.exports = - handler: handler - hooks: - on_action: on_action - metadata: - argument_to_config: 'name' - definitions: definitions diff --git a/packages/system/src/group/read.coffee.md b/packages/system/src/group/read.coffee.md deleted file mode 100644 index b9e6cd4e2..000000000 --- a/packages/system/src/group/read.coffee.md +++ /dev/null @@ -1,104 +0,0 @@ - -# `nikita.system.group.read` - -Read and parse the group definition file located in "/etc/group". - -## Output parameters - -* `groups` - An object where keys are the group names and values are the groups properties. - See the parameter `group` for a list of available properties. -* `group` - Properties associated witht the group, only if the input parameter `gid` is - provided. Available properties are: - * `group` (string) - Name of the group. - * `password` (string) - Group password as a result of the `crypt` function, rarely used. - * `gid` (string) - The numerical equivalent of the group name. It is used by the operating - system and applications when determining access privileges. - * `users` (array[string]) - List of users who are members of this group. - -## Examples - -Retrieve all groups informations: - -```js -const {groups} = await nikita.system.group.read() -console.info("Available groups:", groups) -``` - -Retrieve information of an individual group: - -```js -const {group} = await nikita.system.group.read({ - gid: 1 -}) -console.info("The group found:", group) -``` - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'gid': - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid' - description: ''' - Retrieve the information for a specific group name or gid. - ''' - 'target': - type: 'string' - default: '/etc/group' - description: ''' - Path to the group definition file, default to "/etc/group". - ''' - -## Handler - - handler = ({config, metadata, state, tools: {log}}) -> - config.gid = parseInt config.gid, 10 if typeof config.gid is 'string' and /\d+/.test config.gid - # Parse the groups output - str2groups = (data) -> - groups = {} - for line in utils.string.lines data - line = /(.*)\:(.*)\:(.*)\:(.*)/.exec line - continue unless line - groups[line[1]] = group: line[1], password: line[2], gid: parseInt(line[3]), users: if line[4] then line[4].split ',' else [] - groups - # Fetch the groups information - unless config.target - {stdout} = await @execute - command: 'getent group' - groups = str2groups stdout - else - {data} = await @fs.base.readFile - target: config.target - encoding: 'ascii' - groups = str2groups data - # Return all the groups - return groups: groups unless config.gid - # Return a group by name - if typeof config.gid is 'string' - group = groups[config.gid] - throw Error "Invalid Option: no gid matching #{JSON.stringify config.gid}" unless group - group: group - # Return a group by gid - else - group = Object.values(groups).filter((group) -> group.gid is config.gid)[0] - throw Error "Invalid Option: no gid matching #{JSON.stringify config.gid}" unless group - group: group - -## Exports - - module.exports = - handler: handler - metadata: - definitions: definitions - -## Dependencies - - utils = require '../utils' diff --git a/packages/system/src/info/os.coffee.md b/packages/system/src/info/os.coffee.md deleted file mode 100644 index fa7560c0a..000000000 --- a/packages/system/src/info/os.coffee.md +++ /dev/null @@ -1,80 +0,0 @@ - -# `nikita.system.info.os` - -Expose system information. Internally, it uses the command `uname` to retrieve -information. - -## Todo - -There are more properties exposed by `uname` such as the machine hardware name -and the hardware platform. Those properties shall be exposed. - -We shall explain what "non-portable" means. - -## Example - -```js -const {os} = await nikita.system.info.os() -console.info('Architecture:', os.arch) -console.info('Distribution:', os.distribution) -console.info('Version:', os.version) -console.info('Linux version:', os.linux_version) -``` - -## Schema definitions - -There is no config for this action. - - definitions = - 'output': - type: 'object' - properties: - 'os': - type: 'object' - properties: - 'arch': - type: 'string' - description: ''' - Print the machine architecte, eg `x86_64`, same as `uname -m`. - ''' - 'distribution': - type: 'string' - description: ''' - Linux distribution. Current values include 'rhel', 'centos', - 'ubuntu', 'debian' and 'arch'. - ''' - 'version': - type: 'string' - description: ''' - Version of the distribution, for example '6.10' on CENTOS 6 or - `7.9.2009` on CENTOS 7. - ''' - 'linux_version': - type: 'string' - description: ''' - Linux kernel version, extracted from `uname -r`. - ''' - -## Handler - - handler = -> - # Using `utils.os.command` to be consistant with OS conditions from core - {stdout} = await @execute utils.os.command - [arch, distribution, version, linux_version] = stdout.split '|' - os: - arch: arch - distribution: distribution - version: if version.length then version else undefined # eg Arch Linux - linux_version: linux_version - -## Exports - - module.exports = - handler: handler - metadata: - definitions: definitions - shy: true - -## Dependencies - - utils = require '../utils' diff --git a/packages/system/src/limits.coffee.md b/packages/system/src/limits.coffee.md deleted file mode 100644 index 78f059102..000000000 --- a/packages/system/src/limits.coffee.md +++ /dev/null @@ -1,353 +0,0 @@ - -# `nikita.system.limits` - -Control system limits for a user. - -## Implementation strategy - -### `nproc` and `nofile` - -There are two cases, depending on the specified value: - -1. Integer value - If an int value is specified, then nikita checks that the value is lesser than - the kernel limit. Please be aware that it is necessary but not sufficient to - guarantee that the user would be able to open session. -2. Boolean value - If a true value is specified, then nikita set it to 75% of the kernel limit. - This value is neither optimal nor able to guarantee that the user would be - able to open session, but that is the best nikita can automatically do. - -### Other values - -Other values are not assessed by default. They must be integers. - -## Ulimit - -Linux allows to limit the resources allocated to users or user groups via -"/etc/security/limits.conf" and "/etc/security/limits.d/*.conf" files loaded by -WFP (Plugable Authentication Module) at each logon. The user can then adapt the -resources available to its needs via "ulimit". - -It is possible to define, for a number of resources (number of open files, file size, -number of instantiated process, CPU time, etc.), a "soft" limit which can be -increased by user, via "ulimit" until a maximum "hard" limit. -The system does not exceed the value of the soft limit. If the user wants to push -this limit, it will set a new soft limit with ulimit. -The soft limit is always lower or equal to the hard limit. -In general, the limits applied to a user override those applied to a group. - -## Ulimit commands - -The "S" option to "ulimit" impact the effective limit ("soft" limit) and the "H" -impact the "hard" limit (maximum value that can be defined by the user). - -| resource | soft | hard | unit | -|----------------------|------------|------------|---------| -| core file size | ulimit -Sc | ulimit -Hc | blocks | -| data seg size | ulimit -Sd | ulimit -Hd | kbytes | -| scheduling priority | ulimit -Se | ulimit -He | | -| file size | ulimit -Sf | ulimit -Hf | blocks | -| max locked memory | ulimit -Sl | ulimit -Hl | kbytes | -| pending signals | ulimit -Si | ulimit -Hi | | -| max memory size | ulimit -Sm | ulimit -Hm | kbytes | -| open files | ulimit -Sn | ulimit -Hn | | -| pipe size | ulimit -Sp | ulimit -Hp | bytes | -| POSIX message queues | ulimit -Sq | ulimit -Hq | bytes | -| real-time priority | ulimit -Sr | ulimit -Hr | | -| stack size | ulimit -Ss | ulimit -Hs | kbytes | -| cpu time | ulimit -St | ulimit -Ht | seconds | -| max user processes | ulimit -Su | ulimit -Hu | | -| virtual memory | ulimit -Sv | ulimit -Hv | kbytes | -| file locks | ulimit -Sx | ulimit -Hx | | - -Pass the option in flag-mode to get, and follows it with a value to set. - -## Retrieve current information - -Number of sub-process for a process: - -```bash -pid=14986 -ls /proc/$pid/task | wc -ps -L p $pid --no-headers | wc -l -``` - -Number of sub-process for a user, the option "-L" show threads, possibly with -LWP and NLWP columns: - -```bash -user=`whoami` -ps -L -u $user --no-headers | wc -l -``` - -## Kernel Limits - -User limits cannot exceed kernel limits, so you need to configure kernel limits -before user limits. - -### Processes - -```bash -sysctl kernel.pid_max # print kernel.pid_max = VALUE -cat /proc/sys/kernel/pid_max # print VALUE -``` - -_Temporary change_: `echo 4194303 > /proc/sys/kernel/pid_max` - -_Permanent change_: `vi /etc/sysctl.conf # kernel.pid_max = 4194303` - -### Open Files - -```bash -sysctl fs.file-max # print fs.file-max = VALUE -cat /proc/sys/fs/file-max # print VALUE -``` - -_Temporary change_: `echo 1631017 > /proc/sys/fs/file-max` - -_Permanent change_ : `vi /etc/sysctl.conf # fs.file-max = 1631017` - -## Example - -Setting the number of open file descriptors to .75 of the maximum value for -all the users: - -```js -const {$status} = await nikita.system.limits({ - system: true, - nofile: true -}); -console.log(`Limits modified: ${$status}`); -``` - -## Callback parameters - -* `$status` - Value is "true" if limits configuration file has been modified. - -## Schema definitions - -Refer to the [limits.conf(5)](https://linux.die.net/man/5/limits.conf) Linux man -page for further information. - - definitions = - config: - type: 'object' - properties: - 'as': - $ref: '#/definitions/limits' - description: ''' - Address space limit (KB). - ''' - 'core': - $ref: '#/definitions/limits' - description: ''' - Limits the core file size (KB). - ''' - 'cpu': - $ref: '#/definitions/limits' - description: ''' - CPU time limit (in seconds). When the process reaches the soft limit, - it receives a SIGXCPU every second. When it reaches the hard limit, it - receives SIGKILL. - ''' - 'data': - $ref: '#/definitions/limits' - description: ''' - Max data size (KB). - ''' - 'fsize': - $ref: '#/definitions/limits' - description: ''' - Maximum filesize (KB). - ''' - 'locks': - $ref: '#/definitions/limits' - description: ''' - Max number of file locks the user can hold. - ''' - 'maxlogins': - $ref: '#/definitions/limits' - description: ''' - Max number of logins for this user. - ''' - 'maxsyslogins': - $ref: '#/definitions/limits' - description: ''' - Max number of logins on the system. - ''' - 'memlock': - $ref: '#/definitions/limits' - description: ''' - Max locked-in-memory address space (KB). - ''' - 'msgqueue': - $ref: '#/definitions/limits' - description: ''' - Max memory used by POSIX message queues (bytes). - ''' - 'nice': - oneOf: [ - type: 'integer' - minimum: -20 - maximum: 19 - , - type: 'object' - patternProperties: - '^-|soft|hard$': - type: 'integer' - minimum: -20 - maximum: 19 - additionalProperties: false - ] - description: ''' - Max nice priority allowed to raise to values. - ''' - 'nofile': - $ref: '#/definitions/limits' - description: ''' - Max number of open file descriptors. - ''' - 'nproc': - $ref: '#/definitions/limits' - description: ''' - Max number of processes. - ''' - 'priority': - oneOf: [ - type: 'integer' - , - type: 'object' - patternProperties: - '^-|soft|hard$': type: 'integer' - additionalProperties: false - ] - description: ''' - Priority to run user process with. - ''' - 'rss': - $ref: '#/definitions/limits' - description: ''' - Max resident set size (KB). - ''' - 'sigpending': - $ref: '#/definitions/limits' - description: ''' - Max number of pending signals. - ''' - 'stack': - $ref: '#/definitions/limits' - description: ''' - Max stack size (KB). - ''' - 'rtprio': - $ref: '#/definitions/limits' - description: ''' - Max realtime priority.. - ''' - 'system': - type: 'boolean' - description: ''' - Apply the limits at the system level. - ''' - 'target': - type: 'string' - description: ''' - Where to write the file, default to "/etc/security/limits.conf" for - system limits and "/etc/security/limits.d/#{config.user}.conf" for - user limits. - ''' - 'user': - type: 'string' - description: ''' - The username to who the limit apply, also used for the default target - name. - ''' - oneOf: [ - required: ['system'] - , - required: ['user'] - ] - 'limits': - anyOf: [ - type: ['boolean', 'integer'] - , - type: 'object' - patternProperties: - '^-|soft|hard$': anyOf: [ - type: ['boolean', 'integer'] - , - type: 'string' - enum: ['unlimited'] - ] - additionalProperties: false - , - type: 'string' - enum: ['unlimited'] - ] - -## Handler - - handler = ({config}) -> - throw Error "Incoherent config: both system and user configuration are defined, #{JSON.stringify system: config.system, user: config.user}" if config.system and config.user - config.user = '*' if config.system - throw Error "Missing required option 'user'" unless config.user - config.target ?= "/etc/security/" + if config.user is '*' then "limits.conf" else "limits.d/#{config.user}.conf" - # Calculate nofile from kernel limit - if config.nofile? - {stdout: kern_limit} = await @execute - command: "cat /proc/sys/fs/file-max" - # shy: true - trim: true - if config.nofile is true - config.nofile = Math.round kern_limit * 0.75 - else if typeof config.nofile is 'number' - throw Error "Invalid nofile configuration property. Please set int value lesser than kernel limit: #{kern_limit}" if config.nofile >= kern_limit - else if typeof config.nofile is 'object' - for _, v of config.nofile - throw Error "Invalid nofile configuration property. Please set int value lesser than kernel limit: #{kern_limit}" if v >= kern_limit - # Calculate nproc from kernel limit - if config.nproc? - {stdout: kern_limit} = await @execute - command: "cat /proc/sys/kernel/pid_max" - shy: true - trim: true - if config.nproc is true then config.nproc = Math.round kern_limit * 0.75 - else if typeof config.nproc is 'number' - throw Error "Invalid nproc configuration property. Please set int value lesser than kernel limit: #{kern_limit}" if config.nproc >= kern_limit - else if typeof config.nproc is 'object' - for _, v of config.nproc - throw Error "Invalid nproc configuration property. Please set int value lesser than kernel limit: #{kern_limit}" if v >= kern_limit - # Config normalization - write = [] - for opt in ['as', 'core', 'cpu', 'data', 'fsize', 'locks', 'maxlogins', - 'maxsyslogins', 'memlock', 'msgqueue', 'nice', 'nofile', 'nproc', - 'priority', 'rss', 'sigpending', 'stack', 'rtprio'] - continue unless config[opt]? - config[opt] = '-': config[opt] unless typeof config[opt] is 'object' - for k in Object.keys config[opt] - throw Error "Invalid option: #{JSON.stringify config[opt]}" unless k in ['soft', 'hard', '-'] - throw Error "Invalid option: #{config[opt][k]} not a number" unless (typeof config[opt][k] is 'number') or config[opt][k] is 'unlimited' - write.push - match: RegExp "^#{regexp.escape config.user} +#{regexp.escape k} +#{opt}.+$", 'm' - replace: "#{config.user} #{k} #{opt} #{config[opt][k]}" - append: true - return false unless write.length - @file - target: config.target - write: write - eof: true - uid: config.uid - gid: config.gid - -## Exports - - module.exports = - handler: handler - metadata: - definitions: definitions - -## Dependencies - - {regexp} = require './utils' diff --git a/packages/system/src/mod.coffee.md b/packages/system/src/mod.coffee.md deleted file mode 100644 index f736f8fa4..000000000 --- a/packages/system/src/mod.coffee.md +++ /dev/null @@ -1,114 +0,0 @@ - -# `nikita.system.mod` - -Load a kernel module. By default, unless the `persist` config is "false", -module are loaded on reboot by writing the file "/etc/modules-load.d/{name}.conf". - -## Examples - -Activate the module "vboxpci" in the file "/etc/modules-load.d/vboxpci.conf": - -``` -nikita.system.mod({ - modules: 'vboxpci' -}) -``` - -Activate the module "vboxpci" in the file "/etc/modules-load.d/my_modules.conf": - -``` -nikita.system.mod({ - target: 'my_modules.conf', - modules: 'vboxpci' -}); -``` - -## Hooks - - on_action = ({config}) -> - if typeof config.modules is 'string' - config.modules = [config.modules]: true - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'load': - type: 'boolean' - default: true - description: ''' - Load the module with `modprobe`. - ''' - 'modules': - oneOf: [ - type: 'string' - , - type: 'object' - patternProperties: - '.*': type: 'boolean' - additionalProperties: false - ] - description: ''' - Names of the modules. - ''' - 'persist': - type: 'boolean' - default: true - description: ''' - Load the module on startup by placing a file, see `target`. - ''' - 'target': - type: 'string' - description: ''' - Path of the file to write the module, relative to - "/etc/modules-load.d" unless absolute, default to - "/etc/modules-load.d/{config.modules}.conf". - ''' - required: ['modules'] - -## Handler - - handler = ({metadata, config}) -> - for module, active of config.modules - target = config.target - target ?= "#{module}.conf" - target = path.resolve '/etc/modules-load.d', target - await @execute - $if: config.load and active - command: """ - lsmod | grep #{module} && exit 3 - modprobe #{module} - """ - code: [0, 3] - await @execute - $if: config.load and not active - command: """ - lsmod | grep #{module} || exit 3 - modprobe -r #{module} - """ - code: [0, 3] - await @file - $if: config.persist - target: target - match: ///^#{quote module}(\n|$)///mg - replace: if active then "#{module}\n" else '' - append: true - eof: true - undefined - -## Exports - - module.exports = - handler: handler - hooks: - on_action: on_action - metadata: - definitions: definitions - argument_to_config: 'modules' - -## Dependencies - - path = require 'path' - quote = require 'regexp-quote' diff --git a/packages/system/src/register.coffee b/packages/system/src/register.coffee deleted file mode 100644 index 35c818db7..000000000 --- a/packages/system/src/register.coffee +++ /dev/null @@ -1,32 +0,0 @@ - -# Registration of `nikita.system` actions - -require '@nikitajs/file/lib/register' -registry = require '@nikitajs/core/lib/registry' - -module.exports = - system: - cgroups: '@nikitajs/system/src/cgroups' - group: - '': '@nikitajs/system/src/group' - read: '@nikitajs/system/src/group/read' - remove: '@nikitajs/system/src/group/remove' - info: - disks: '@nikitajs/system/src/info/disks' - os: '@nikitajs/system/src/info/os' - limits: '@nikitajs/system/src/limits' - mod: '@nikitajs/system/src/mod' - running: '@nikitajs/system/src/running' - tmpfs: '@nikitajs/system/src/tmpfs' - uid_gid: '@nikitajs/system/src/uid_gid' - user: - '': '@nikitajs/system/src/user' - read: '@nikitajs/system/src/user/read' - remove: '@nikitajs/system/src/user/remove' -(-> - try - await registry.register module.exports - catch err - console.error err.stack - process.exit(1) -)() diff --git a/packages/system/src/running.coffee.md b/packages/system/src/running.coffee.md deleted file mode 100644 index a54e9633e..000000000 --- a/packages/system/src/running.coffee.md +++ /dev/null @@ -1,101 +0,0 @@ - -# `nikita.system.running` - -Check if a process is running. - -## Check if the pid is running - -The example check if a pid match a running process. - -```js -const {$status} = await nikita.system.running({ - pid: 1034, -}) -console.info(`Is PID running: ${$status}`) -``` - -## Check if the pid stored in a file is running - -The example read a file and check if the pid stored inside is currently running. -This pattern is used by YUM and APT to create lock files. The target file will -be removed if it stores a value not matching a running pid. - -```js -const {$status} = await nikita.system.running({ - target: '/var/run/yum.pid' -}) -console.info(`Is PID running: ${$status}`) -``` - -## Hooks - - on_action = ({config}) -> - config.pid = parseInt config.pid, 10 if typeof config.pid is 'string' - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'pid': - type: 'integer' - description: ''' - The PID of the process to inspect, required if `target` is not provided. - ''' - 'target': - type: 'string' - description: ''' - Path to the file storing the PID value, required if `pid` is not provided. - ''' - anyOf: [ - required: ['pid'] - , - required: ['target'] - ] - -## Handler - - handler = ({config, tools: {log}}) -> - # Validate parameters - throw Error 'Invalid Options: one of pid or target must be provided' unless config.pid? or config.target - throw Error 'Invalid Options: either pid or target must be provided' if config.pid? and config.target - if config.pid - {code} = await @execute - command: """ - kill -s 0 '#{config.pid}' >/dev/null 2>&1 || exit 42 - """ - code: [0, 42] - log switch code - when 0 then message: "PID #{config.pid} is running", level: 'INFO', module: 'nikita/lib/system/running' - when 42 then message: "PID #{config.pid} is not running", level: 'INFO', module: 'nikita/lib/system/running' - return running: true if code is 0 - if config.target - {code, stdout} = await @execute - command: """ - [ -f '#{config.target}' ] || exit 43 - pid=`cat '#{config.target}'` - echo $pid - if ! kill -s 0 "$pid" >/dev/null 2>&1; then - rm '#{config.target}'; - exit 42; - fi - """ - code: [0, [42, 43]] - stdout_trim: true - log switch code - when 0 then message: "PID #{stdout} is running", level: 'INFO', module: 'nikita/lib/system/running' - when 42 then message: "PID #{stdout} is not running", level: 'INFO', module: 'nikita/lib/system/running' - when 43 then message: "PID file #{config.target} does not exists", level: 'INFO', module: 'nikita/lib/system/running' - return running: true if code is 0 - running: false - -## Exports - - module.exports = - handler: handler - hooks: - on_action: on_action - metadata: - # raw_output: true - definitions: definitions diff --git a/packages/system/src/tmpfs.coffee.md b/packages/system/src/tmpfs.coffee.md deleted file mode 100644 index 456b8f8da..000000000 --- a/packages/system/src/tmpfs.coffee.md +++ /dev/null @@ -1,140 +0,0 @@ - -# `nikita.system.tmpfs` - -Mount a directory with tmpfs.d as a [tmpfs](https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html) configuration file. - -## Callback parameters - -* `$status` - Wheter the directory was mounted or already mounted. - -# Example - -All parameters can be omitted except type. nikita.tmpfs will ommit by replacing -the undefined value as '-', which does apply the os default behavior. - -Setting uid/gid to '-', make the os creating the target owned by root:root. - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'age': - type: 'string' - description: ''' - Used to decide what files to delete when cleaning. - ''' - 'argument': - type: 'string' - description: ''' - The destination path of the symlink if type is `L`. - ''' - 'backup': - type: ['boolean', 'string'] - default: true - description: ''' - Create a backup, append a provided string to the filename extension or - a timestamp if value is not a string, only apply if the target file - exists and is modified. - ''' - 'gid': - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid' - description: ''' - File group name or group id. - ''' - 'merge': - type: 'boolean' - default: true - description: ''' - Overrides properties if already exits. - ''' - 'mount': - type: 'string' - description: ''' - The mount point dir to create on system startup. - ''' - 'mode': - $ref: 'module://@nikitajs/core/lib/actions/fs/chmod#/definitions/config/properties/mode' - description: ''' - Mode of the target configuration file - ''' - 'name': - type: 'string' - description: ''' - The file name, can not be used with target. If only `name` is set, it - writes the content to default configuration directory and creates the - file as '`name`.conf'. - ''' - 'perm': - type: 'string' - default: '0644' - description: ''' - Mount path mode in string format like `"0644"`. - ''' - 'target': - type: 'string' - description: ''' - File path where to write content to. Defined to - /etc/tmpfiles.d/{config.uid}.conf if uid is defined or - /etc/tmpfiles.d/default.conf. - ''' - 'uid': - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid' - description: ''' - File user name or user id. - ''' - required: ['mount'] - -## Handler - - handler = ({config, tools: {log}}) -> - # for now only support directory type path option - content = {} - content[config.mount] = {} - content[config.mount][key] = config[key] for key in ['mount','perm','uid','gid','age','argu'] - content[config.mount]['type'] = 'd' - if config.uid? - config.name ?= config.uid unless /^[0-9]+/.exec config.uid - config.target ?= if config.name? then "/etc/tmpfiles.d/#{config.name}.conf" else '/etc/tmpfiles.d/default.conf' - log message: "target set to #{config.target}", level: 'DEBUG' - if config.merge - log message: "opening target file for merge", level: 'DEBUG' - try - {data} = await @fs.base.readFile target: config.target, encoding: 'utf8' - source = utils.tmpfs.parse data - content = merge source, content - log message: "content has been merged", level: 'DEBUG' - catch err - throw err unless err.code is 'NIKITA_FS_CRS_TARGET_ENOENT' - # Serialize and write the content - content = utils.tmpfs.stringify(content) - {$status} = await @file - # $debug: true - content: content - gid: config.gid - mode: config.mode - target: config.target - uid: config.uid - if $status - log message: "re-creating #{config.mount} tmpfs file", level: 'INFO' - await @execute - command: "systemd-tmpfiles --remove #{config.target}" - await @execute - command: "systemd-tmpfiles --create #{config.target}" - undefined - -## Exports - - module.exports = - handler: handler - metadata: - definitions: definitions - -## Dependencies - - utils = require './utils' - {merge} = require 'mixme' - -[conf-tmpfs]: https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html diff --git a/packages/system/src/uid_gid.coffee.md b/packages/system/src/uid_gid.coffee.md deleted file mode 100644 index ac47ecceb..000000000 --- a/packages/system/src/uid_gid.coffee.md +++ /dev/null @@ -1,75 +0,0 @@ - -# `nikita.system.uid_gid` - -Normalize the "uid" and "gid" properties. A username defined by the "uid" option will -be converted to a Unix user ID and a group defined by the "gid" option will -be converted to a Unix group ID. - -At the moment, this only work with Unix username because it only read the -"/etc/passwd" file. A future implementation might execute a system command to -retrieve information from external identity providers. - -## Exemple - -```js -const {uid, gid} = await nikita.system.uid_gid({ - uid: 'myuser', - gid: 'mygroup' -}) -console.info(`User uid is ${config.uid}`) -console.info(`Group gid is ${config.gid}`) -``` - -## Hooks - - on_action = ({config}) -> - config.uid = parseInt config.uid, 10 if typeof config.uid is 'string' and /^\d+$/.test config.uid - config.gid = parseInt config.gid, 10 if typeof config.gid is 'string' and /^\d+$/.test config.gid - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'gid': - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/gid' - 'group_target': - type: 'string' - description: ''' - Path to the group definition file, default to "/etc/group" - ''' - 'passwd_target': - type: 'string' - description: ''' - Path to the passwd definition file, default to "/etc/passwd". - ''' - 'uid': - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid' - -## Handler - - handler = ({config}, callback) -> - if config.uid and typeof config.uid is 'string' - {user} = await @system.user.read - target: config.passwd_target - uid: config.uid - config.uid = user.uid - config.default_gid = user.gid - if config.gid and typeof config.gid is 'string' - {group} = await @system.group.read - target: config.group_target - gid: config.gid - config.gid = group.gid - uid: config.uid - gid: config.gid - default_gid: config.default_gid - -## Exports - - module.exports = - handler: handler - hooks: - on_action: on_action - metadata: - definitions: definitions diff --git a/packages/system/src/user/index.coffee.md b/packages/system/src/user/index.coffee.md deleted file mode 100644 index 06a24f7fd..000000000 --- a/packages/system/src/user/index.coffee.md +++ /dev/null @@ -1,249 +0,0 @@ - -# `nikita.system.user` - -Create or modify a Unix user. - -If the user home is provided, its parent directory will be created with root -ownerships and 0644 permissions unless it already exists. - -## Callback parameters - -* `$status` - Value is "true" if user was created or modified. - -## Example - -```js -const {$status} = await nikita.system.user({ - name: 'a_user', - system: true, - uid: 490, - gid: 10, - comment: 'A System User' -}) -console.log(`User created: ${$status}`) -``` - -The result of the above action can be viewed with the command -`cat /etc/passwd | grep myself` producing an output similar to -"a\_user:x:490:490:A System User:/home/a\_user:/bin/bash". You can also check -you are a member of the "wheel" group (gid of "10") with the command -`id a\_user` producing an output similar to -"uid=490(hive) gid=10(wheel) groups=10(wheel)". - -## Hooks - - on_action = ({config}) -> - switch config.shell - when true - config.shell = '/bin/sh' - when false - config.shell = '/sbin/nologin' - config.groups = config.groups.split ',' if typeof config.groups is 'string' - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'comment': - type: 'string' - description: ''' - Short description of the login. - ''' - 'expiredate': - type: 'integer' - description: ''' - The date on which the user account is disabled. - ''' - 'gid': - type: 'integer' - description: ''' - Group name or number of the user´s initial login group. - ''' - 'groups': - type: 'array' - items: type: 'string' - description: ''' - List of supplementary groups which the user is also a member of. - ''' - 'home': - type: 'string' - description: ''' - Value for the user´s login directory, default to the login name - appended to "BASE_DIR". - ''' - 'inactive': - type: 'integer' - description: ''' - The number of days after a password has expired before the account - will be disabled. - ''' - 'name': - type: 'string' - description: ''' - Login name of the user. - ''' - 'no_home_ownership': - type: 'boolean' - description: ''' - Disable ownership on home directory which default to the "uid" and - "gid" config, default is "false". - ''' - 'password': - type: 'string' - description: ''' - The unencrypted password. - ''' - 'password_sync': - type: 'boolean' - default: true - description: ''' - Synchronize password - ''' - 'shell': - # oneOf: [ - # type: 'boolean' - # , - # type: 'string' - # ] - type: ['boolean', 'string'] - default: '/bin/sh' - description: ''' - Path to the user shell, set to "/sbin/nologin" if `false` and "/bin/sh" - if `true` or `undefined`. - ''' - 'skel': - type: 'string' - description: ''' - The skeleton directory, which contains files and directories to be - copied in the user´s home directory, when the home directory is - created by useradd. - ''' - 'system': - type: 'boolean' - description: ''' - Create a system account, such user are not created with a home by - default, set the "home" option if we it to be created. - ''' - 'uid': - type: 'integer' - description: ''' - Numerical value of the user´s ID, must not exist. - ''' - required: ['name'] - -## Handler - - handler = ({metadata, config, tools: {log}}) -> - log message: "Entering user", level: 'DEBUG' - config.system ?= false - config.gid ?= null - config.password_sync ?= true - throw Error "Invalid option 'shell': #{JSON.strinfigy config.shell}" if config.shell? typeof config.shell isnt 'string' - user_info = groups_info = null - {users} = await @system.user.read() - user_info = users[config.name] - log if user_info - then message: "Got user information for #{JSON.stringify config.name}", level: 'DEBUG', module: 'nikita/lib/system/group' - else message: "User #{JSON.stringify config.name} not present", level: 'DEBUG', module: 'nikita/lib/system/group' - # Get group information if - # * user already exists - # * we need to compare groups membership - {groups} = await @system.group.read - $if: user_info and config.groups - groups_info = groups - log message: "Got group information for #{JSON.stringify config.name}", level: 'DEBUG' if groups_info - if config.home - @fs.mkdir - $unless_exists: path.dirname config.home - target: path.dirname config.home - uid: 0 - gid: 0 - mode: 0o0644 # Same as '/home' - unless user_info - await @execute [ - code: [0, 9] - command: [ - 'useradd' - '-r' if config.system - '-M' unless config.home - '-m' if config.home - "-d #{config.home}" if config.home - "-s #{config.shell}" if config.shell - "-c #{utils.string.escapeshellarg config.comment}" if config.comment - "-u #{config.uid}" if config.uid - "-g #{config.gid}" if config.gid - "-e #{config.expiredate}" if config.expiredate - "-f #{config.inactive}" if config.inactive - "-G #{config.groups.join ','}" if config.groups - "-k #{config.skel}" if config.skel - "#{config.name}" - ].join ' ' - , - $if: config.home - command: "chown #{config.name}. #{config.home}" - ] - log message: "User defined elsewhere than '/etc/passwd', exit code is 9", level: 'WARN' - else - changed = [] - for k in ['uid', 'home', 'shell', 'comment', 'gid'] - changed.push k if config[k]? and user_info[k] isnt config[k] - if config.groups then for group in config.groups - throw Error "Group does not exist: #{group}" unless groups_info[group] - changed.push 'groups' if groups_info[group].users.indexOf(config.name) is -1 - log if changed.length - then message: "User #{config.name} modified", level: 'WARN', module: 'nikita/lib/system/user/add' - else message: "User #{config.name} not modified", level: 'DEBUG', module: 'nikita/lib/system/user/add' - try - await @execute - $if: changed.length - command: [ - 'usermod' - "-d #{config.home}" if config.home - "-s #{config.shell}" if config.shell - "-c #{utils.string.escapeshellarg config.comment}" if config.comment? - "-g #{config.gid}" if config.gid - "-G #{config.groups.join ','}" if config.groups - "-u #{config.uid}" if config.uid - "#{config.name}" - ].join ' ' - catch err - if err.exit_code is 8 - throw Error "User #{config.name} is logged in" - else throw err - if config.home and (config.uid or config.gid) - await @fs.chown - $if_exists: config.home - $unless: config.no_home_ownership - target: config.home - uid: config.uid - gid: config.gid - # TODO, detect changes in password - # echo #{config.password} | passwd --stdin #{config.name} - if config.password_sync and config.password - {$status} = await @execute - command: """ - hash=$(echo #{config.password} | openssl passwd -1 -stdin) - usermod --pass="$hash" #{config.name} - """ - # arch_chroot: config.arch_chroot - # rootdir: config.rootdir - # sudo: config.sudo - log message: "Password modified", level: 'WARN' if $status - -## Exports - - module.exports = - handler: handler - hooks: - on_action: on_action - metadata: - argument_to_config: 'name' - definitions: definitions - -## Dependencies - - path = require 'path' - utils = require '../utils' diff --git a/packages/system/src/user/read.coffee.md b/packages/system/src/user/read.coffee.md deleted file mode 100644 index efc1e2f95..000000000 --- a/packages/system/src/user/read.coffee.md +++ /dev/null @@ -1,111 +0,0 @@ - -# `nikita.system.user.read` - -Read and parse the passwd definition file located in "/etc/passwd". - -## Output parameters - -* `users` - An object where keys are the usernames and values are the user properties. - See the parameter `user` for a list of available properties. -* `user` - Properties associated witht the user, only if the input parameter `uid` is - provided. Available properties are: - * `user` (string) - Username. - * `uid` (integer) - User Id. - * `comment` (string) - User description - * `home` (string) - User home directory. - * `shell` (string) - Default user shell command. - -## Example - -```js -const {$status, users} = await nikita -.file({ - target: "/tmp/etc/passwd", - content: "root:x:0:0:root:/root:/bin/bash" -}) -.system.user.read({ - target: "/tmp/etc/passwd" -}) -assert.equal($status, false) -assert.deepEqual(users, { - "root": { user: 'root', uid: 0, gid: 0, comment: 'root', home: '/root', shell: '/bin/bash' } -}) -``` - -## Implementation - -The default implementation use the `getent passwd` command. It is possible to -read an alternative `/etc/passwd` file by setting the `target` option to the -targeted file. - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - 'target': - type: 'string' - description: ''' - Path to the passwd definition file, use the `getent passwd` command by - default which use to "/etc/passwd". - ''' - 'uid': - $ref: 'module://@nikitajs/core/lib/actions/fs/chown#/definitions/config/properties/uid' - description: ''' - Retrieve the information for a specific username or uid. - ''' - -## Handler - - handler = ({config}) -> - config.uid = parseInt config.uid, 10 if typeof config.uid is 'string' and /\d+/.test config.uid - # Parse the passwd output - str2passwd = (data) -> - passwd = {} - for line in utils.string.lines data - line = /(.*)\:\w\:(.*)\:(.*)\:(.*)\:(.*)\:(.*)/.exec line - continue unless line - passwd[line[1]] = user: line[1], uid: parseInt(line[2]), gid: parseInt(line[3]), comment: line[4], home: line[5], shell: line[6] - passwd - # Fetch the users information - unless config.target - {stdout} = await @execute - command: 'getent passwd' - passwd = str2passwd stdout - else - {data} = await @fs.base.readFile - target: config.target - encoding: 'ascii' - passwd = str2passwd data - # Return all the users - return users: passwd unless config.uid - # Return a user by username - if typeof config.uid is 'string' - user = passwd[config.uid] - throw Error "Invalid Option: no uid matching #{JSON.stringify config.uid}" unless user - user: user - # Return a user by uid - else - user = Object.values(passwd).filter((user) -> user.uid is config.uid)[0] - throw Error "Invalid Option: no uid matching #{JSON.stringify config.uid}" unless user - user: user - -## Exports - - module.exports = - handler: handler - metadata: - definitions: definitions - shy: true - -## Dependencies - - utils = require '../utils' diff --git a/packages/system/src/user/remove.coffee.md b/packages/system/src/user/remove.coffee.md deleted file mode 100644 index d37413aad..000000000 --- a/packages/system/src/user/remove.coffee.md +++ /dev/null @@ -1,46 +0,0 @@ - -# `nikita.system.user.remove` - -Create or modify a Unix user. - -## Callback parameters - -* `$status` - Value is "true" if user was created or modified. - -## Example - -```js -const {$status} = await nikita.system.user.remove({ - name: 'a_user' -}) -console.log(`User removed: ${$status}`) -``` - -## Schema definitions - - definitions = - config: - type: 'object' - properties: - name: - type: 'string' - description: ''' - Name of the user to removed. - ''' - required: ['name'] - -## Handler - - handler = ({config}) -> - @execute - command: "userdel #{config.name}" - code: [0, 6] - -## Exports - - module.exports = - handler: handler - metadata: - argument_to_config: 'name' - definitions: definitions diff --git a/packages/system/src/utils/cgconfig.coffee b/packages/system/src/utils/cgconfig.coffee deleted file mode 100644 index 3d63d6ab3..000000000 --- a/packages/system/src/utils/cgconfig.coffee +++ /dev/null @@ -1,145 +0,0 @@ - -utils = require '@nikitajs/core/lib/utils' - -module.exports = - parse: (str) -> - lines = utils.string.lines str - list_of_mount_sections = [] - list_of_group_sections = {} - # variable which hold the cursor position - current_mount = false - current_group = false - current_group_name = '' - current_group_controller = false - current_group_perm = false - current_group_perm_content = false - current_default = false - # variables which hold the data - current_mount_section = null - current_group_section = null # group section is a tree but only of group - current_controller_name = null - current_group_section_perm_name = null - lines.forEach (line, _, __) -> - return if not line or line.match(/^\s*$/) - if !current_mount and !current_group and !current_default - if /^mount\s{$/.test line # start of a mount object - current_mount = true - current_mount_section = [] - if /^(group)\s([A-z|0-9|\/]*)\s{$/.test line # start of a group object - current_group = true - match = /^(group)\s([A-z|0-9|\/]*)\s{$/.exec line - current_group_name = match[2] - current_group_section = {} - list_of_group_sections["#{current_group_name}"] ?= {} - if /^(default)\s{$/.test line # start of a special group object named default - current_group = true - current_group_name = '' - current_group_section = {} - list_of_group_sections["#{current_group_name}"] ?= {} - else - # we are parsing a mount object - # ^(cpuset|cpu|cpuacct|memory|devices|freezer|net_cls|blkio)\s=\s[aA-zZ|\s]* - if current_mount - if /^}$/.test line # close the mount object - list_of_mount_sections.push current_mount_section... - current_mount = false - current_mount_section = [] - # add the line to mont object - else - line = line.replace ';','' - sep = '=' - sep = ':' if line.indexOf(':') isnt -1 - line = line.split sep - current_mount_section.push type: "#{line[0].trim()}", path:"#{line[1].trim()}" - # we are parsing a group object - # ^(cpuset|cpu|cpuacct|memory|devices|freezer|net_cls|blkio)\s=\s[aA-zZ|\s]* - if current_group - # if a closing bracket is encountered, it should set the cursor to false - if /^(\s*)?}$/.test line - if current_group - if current_group_controller - current_group_controller = false - else if current_group_perm - if current_group_perm_content - current_group_perm_content = false - else - current_group_perm = false - else - current_group = false - # push the group if the closing bracket is closing a group - # list_of_group_sections["#{current_group_name}"] = current_group_section - current_group_section = null - #closing the group object - else - match = /^\s*(cpuset|cpu|cpuacct|blkio|memory|devices|freezer|net_cls|perf_event|net_prio|hugetlb|pids|rdma)\s{$/.exec line - # currently reading a group config - if !current_group_perm and !current_group_controller - #if neither working in perm or controller section, we are declaring one of them - if /^\s*perm\s{$/.test line # perm declaration - current_group_perm = true - current_group_section['perm'] = {} - list_of_group_sections["#{current_group_name}"]['perm'] = {} - if match #controller declaration - current_group_controller = true - current_controller_name = match[1] - current_group_section["#{current_controller_name}"] = {} - list_of_group_sections["#{current_group_name}"]["#{current_controller_name}"] ?= {} - else if current_group_perm and current_group_perm_content# perm config - line = line.replace ';','' - line = line.split('=') - [type,value] = line - current_group_section['perm'][current_group_section_perm_name][type.trim()] = value.trim() - list_of_group_sections["#{current_group_name}"]['perm'][current_group_section_perm_name][type.trim()] = value.trim() - else if current_group_controller # controller config - line = line.replace ';','' - sep = '=' - sep = ':' if line.indexOf(':') isnt -1 - line = line.split sep - [type, value] = line - list_of_group_sections["#{current_group_name}"]["#{current_controller_name}"][type.trim()] ?= value.trim() - else - match_admin = /^\s*(admin|task)\s{$/.exec line - if match_admin # admin or task declaration - [_,name] = match_admin #the name is either admin or task - current_group_perm_content = true - current_group_section_perm_name = name - current_group_section['perm'][name] = {} - list_of_group_sections["#{current_group_name}"]['perm'][name] = {} - mounts: list_of_mount_sections, groups: list_of_group_sections - stringify: (obj, config={}) -> - obj.mounts ?= [] - obj.groups ?= {} - render = "" - config.indent ?= 2 - indent = '' - indent += ' ' for i in [1..config.indent] - sections = [] - if obj.mounts.length isnt 0 - mount_render = "mount {\n" - for mount,k in obj.mounts - mount_render += "#{indent}#{mount.type} = #{mount.path};\n" - mount_render += '}' - sections.push mount_render - count = 0 - for name, group of obj.groups - group_render = if (name is '') or (name is 'default') then 'default {\n' else "group #{name} {\n" - for key, value of group - if key is 'perm' - group_render += "#{indent}perm {\n" - if value['admin']? - group_render += "#{indent}#{indent}admin {\n" - group_render += "#{indent}#{indent}#{indent}#{prop} = #{val};\n" for prop, val of value['admin'] - group_render += "#{indent}#{indent}}\n" - if value['task']? - group_render += "#{indent}#{indent}task {\n" - group_render += "#{indent}#{indent}#{indent}#{prop} = #{val};\n" for prop, val of value['task'] - group_render += "#{indent}#{indent}}\n" - group_render += "#{indent}}\n" - else - group_render += "#{indent}#{key} {\n" - group_render += "#{indent}#{indent}#{prop} = #{val};\n" for prop, val of value - group_render += "#{indent}}\n" - group_render += '}' - count++ - sections.push group_render - sections.join "\n" diff --git a/packages/system/src/utils/index.coffee b/packages/system/src/utils/index.coffee deleted file mode 100644 index 26fd82f00..000000000 --- a/packages/system/src/utils/index.coffee +++ /dev/null @@ -1,7 +0,0 @@ -utils = require '@nikitajs/core/lib/utils' - -module.exports = { - ...utils - cgconfig: require './cgconfig' - tmpfs: require './tmpfs' -} diff --git a/packages/system/src/utils/tmpfs.coffee b/packages/system/src/utils/tmpfs.coffee deleted file mode 100644 index 4fa487d3d..000000000 --- a/packages/system/src/utils/tmpfs.coffee +++ /dev/null @@ -1,25 +0,0 @@ - -# parse the content of tmpfs daemon configuration file - -string = require '@nikitajs/core/lib/utils/string' - -module.exports = - parse: (str) -> - lines = string.lines str - files = {} - lines.forEach (line, _, __) -> - return if not line or line.match(/^#.*$/) - values = [type,mount,mode,uid,gid,age,argu] = line.split(/\s+/) - obj = {} - for i,key of ['type','mount','perm','uid','gid','age','argu'] - obj[key] = if values[i] isnt undefined then values[i] else '-' - if i is "#{values.length-1}" - files[mount] = obj if obj['mount']? - files - stringify: (obj) -> - lines = [] - for k, v of obj - for i,key of ['mount','perm','uid','gid','age','argu'] - v[key] = if v[key] isnt undefined then v[key] else '-' - lines.push "#{v.type} #{v.mount} #{v.perm} #{v.uid} #{v.gid} #{v.age} #{v.argu}" - lines.join '\n' diff --git a/packages/system/test/cgroups.coffee b/packages/system/test/cgroups.coffee index fc28e21ff..4e7002649 100644 --- a/packages/system/test/cgroups.coffee +++ b/packages/system/test/cgroups.coffee @@ -1,5 +1,5 @@ -utils = require '../src/utils' +utils = require '../lib/utils' nikita = require '@nikitajs/core/lib' {tags, config} = require './test' they = require('mocha-they')(config)