@katmer/core 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/cli/katmer.js +28 -0
- package/cli/run.ts +16 -0
- package/index.ts +5 -0
- package/lib/config.ts +82 -0
- package/lib/interfaces/config.interface.ts +113 -0
- package/lib/interfaces/executor.interface.ts +13 -0
- package/lib/interfaces/module.interface.ts +170 -0
- package/lib/interfaces/provider.interface.ts +214 -0
- package/lib/interfaces/task.interface.ts +100 -0
- package/lib/katmer.ts +126 -0
- package/lib/lookup/env.lookup.ts +13 -0
- package/lib/lookup/file.lookup.ts +23 -0
- package/lib/lookup/index.ts +46 -0
- package/lib/lookup/url.lookup.ts +21 -0
- package/lib/lookup/var.lookup.ts +13 -0
- package/lib/module.ts +560 -0
- package/lib/module_registry.ts +64 -0
- package/lib/modules/apt-repository/apt-repository.module.ts +435 -0
- package/lib/modules/apt-repository/apt-sources-list.ts +363 -0
- package/lib/modules/apt.module.ts +546 -0
- package/lib/modules/archive.module.ts +280 -0
- package/lib/modules/become.module.ts +119 -0
- package/lib/modules/copy.module.ts +807 -0
- package/lib/modules/cron.module.ts +541 -0
- package/lib/modules/debug.module.ts +231 -0
- package/lib/modules/gather_facts.module.ts +605 -0
- package/lib/modules/git.module.ts +243 -0
- package/lib/modules/hostname.module.ts +213 -0
- package/lib/modules/http/http.curl.module.ts +342 -0
- package/lib/modules/http/http.local.module.ts +253 -0
- package/lib/modules/http/http.module.ts +298 -0
- package/lib/modules/index.ts +14 -0
- package/lib/modules/package.module.ts +283 -0
- package/lib/modules/script.module.ts +121 -0
- package/lib/modules/set_fact.module.ts +171 -0
- package/lib/modules/systemd_service.module.ts +373 -0
- package/lib/modules/template.module.ts +478 -0
- package/lib/providers/local.provider.ts +336 -0
- package/lib/providers/provider_response.ts +20 -0
- package/lib/providers/ssh/ssh.provider.ts +420 -0
- package/lib/providers/ssh/ssh.utils.ts +31 -0
- package/lib/schemas/katmer_config.schema.json +358 -0
- package/lib/target_resolver.ts +298 -0
- package/lib/task/controls/environment.control.ts +42 -0
- package/lib/task/controls/index.ts +13 -0
- package/lib/task/controls/loop.control.ts +89 -0
- package/lib/task/controls/register.control.ts +23 -0
- package/lib/task/controls/until.control.ts +64 -0
- package/lib/task/controls/when.control.ts +25 -0
- package/lib/task/task.ts +225 -0
- package/lib/utils/ajv.utils.ts +24 -0
- package/lib/utils/cls.ts +4 -0
- package/lib/utils/datetime.utils.ts +15 -0
- package/lib/utils/errors.ts +25 -0
- package/lib/utils/execute-shell.ts +116 -0
- package/lib/utils/file.utils.ts +68 -0
- package/lib/utils/http.utils.ts +10 -0
- package/lib/utils/json.utils.ts +15 -0
- package/lib/utils/number.utils.ts +9 -0
- package/lib/utils/object.utils.ts +11 -0
- package/lib/utils/os.utils.ts +31 -0
- package/lib/utils/path.utils.ts +9 -0
- package/lib/utils/renderer/render_functions.ts +3 -0
- package/lib/utils/renderer/renderer.ts +89 -0
- package/lib/utils/renderer/twig.ts +191 -0
- package/lib/utils/string.utils.ts +33 -0
- package/lib/utils/typed-event-emitter.ts +26 -0
- package/lib/utils/unix.utils.ts +91 -0
- package/lib/utils/windows.utils.ts +92 -0
- package/package.json +67 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ModuleCommonReturn,
|
|
3
|
+
type ModuleConstraints
|
|
4
|
+
} from "../interfaces/module.interface"
|
|
5
|
+
import type { Katmer } from "../interfaces/task.interface"
|
|
6
|
+
import type { KatmerProvider } from "../interfaces/provider.interface"
|
|
7
|
+
import { evalExpr, evalIterative } from "../utils/renderer/renderer"
|
|
8
|
+
import { toMerged } from "es-toolkit"
|
|
9
|
+
import { KatmerModule } from "../module"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Allow task syntax:
|
|
13
|
+
*
|
|
14
|
+
* - name: compute values
|
|
15
|
+
* set_fact:
|
|
16
|
+
* vars:
|
|
17
|
+
* release_dir: "{{ app_dir }}/releases/{{ release }}"
|
|
18
|
+
* stamp: "{{ 1 + 2 }}"
|
|
19
|
+
* render: true
|
|
20
|
+
*/
|
|
21
|
+
declare module "../interfaces/task.interface" {
|
|
22
|
+
export namespace Katmer {
|
|
23
|
+
export interface TaskActions {
|
|
24
|
+
/** Compute and set variables (facts) on the task context. */
|
|
25
|
+
set_fact?: SetFactModuleOptions
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Options for the set_fact module.
|
|
32
|
+
*
|
|
33
|
+
* You can pass either:
|
|
34
|
+
* - a plain object of key/value pairs, or
|
|
35
|
+
* - `{ vars, render, deep }` for more control
|
|
36
|
+
*
|
|
37
|
+
* When `render` is true, string values that contain templates like `{{ ... }}` are
|
|
38
|
+
* evaluated using the current `ctx.variables` scope. When `deep` is true, objects
|
|
39
|
+
* and arrays are traversed and any string leaves are evaluated similarly.
|
|
40
|
+
*
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
export type SetFactModuleOptions =
|
|
44
|
+
| Record<string, unknown>
|
|
45
|
+
| {
|
|
46
|
+
/** Key/value pairs to set on `ctx.variables`. */
|
|
47
|
+
vars: Record<string, unknown>
|
|
48
|
+
/**
|
|
49
|
+
* Evaluate string templates with `evalExpr`.
|
|
50
|
+
* Only strings that look like templates are evaluated.
|
|
51
|
+
* @defaultValue true
|
|
52
|
+
*/
|
|
53
|
+
render?: boolean
|
|
54
|
+
/**
|
|
55
|
+
* Recursively render nested objects/arrays.
|
|
56
|
+
* Only impacts rendering when `render=true`.
|
|
57
|
+
* @defaultValue false
|
|
58
|
+
*/
|
|
59
|
+
deep?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Result of the set_fact module.
|
|
64
|
+
* @public
|
|
65
|
+
*/
|
|
66
|
+
export interface SetFactModuleResult extends ModuleCommonReturn {
|
|
67
|
+
/** The facts that were set (post-render). */
|
|
68
|
+
facts: Record<string, unknown>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Compute and set variables (facts) on the task context.
|
|
73
|
+
*
|
|
74
|
+
* @remarks
|
|
75
|
+
* - Values are merged into `ctx.variables`.
|
|
76
|
+
* - `changed` is true when a value is added or changed.
|
|
77
|
+
* - When `render=true`, string values that contain `{{ ... }}` are evaluated via `evalExpr`
|
|
78
|
+
* with `ctx.variables` as scope. Set `deep=true` to render nested strings as well.
|
|
79
|
+
*
|
|
80
|
+
* @examples
|
|
81
|
+
* ```yaml
|
|
82
|
+
* - name: compute derived paths and flags
|
|
83
|
+
* set_fact:
|
|
84
|
+
* vars:
|
|
85
|
+
* app_dir: /opt/myapp
|
|
86
|
+
* release: "2025-01-01"
|
|
87
|
+
* release_dir: "{{ app_dir }}/releases/{{ release }}"
|
|
88
|
+
* is_prod: "{{ env == 'prod' }}"
|
|
89
|
+
* nested:
|
|
90
|
+
* a: "value"
|
|
91
|
+
* b: "{{ app_dir }}/current"
|
|
92
|
+
* render: true
|
|
93
|
+
* deep: true
|
|
94
|
+
*
|
|
95
|
+
* - name: shorthand object (equivalent to vars: {...})
|
|
96
|
+
* set_fact:
|
|
97
|
+
* BUILD_ID: "42"
|
|
98
|
+
* url: "https://example.com/{{ BUILD_ID }}"
|
|
99
|
+
* # with render=true (default), url becomes "https://example.com/42"
|
|
100
|
+
* ```
|
|
101
|
+
*/
|
|
102
|
+
export class SetFactModule extends KatmerModule<
|
|
103
|
+
SetFactModuleOptions,
|
|
104
|
+
SetFactModuleResult,
|
|
105
|
+
KatmerProvider
|
|
106
|
+
> {
|
|
107
|
+
static name = "set_fact" as const
|
|
108
|
+
|
|
109
|
+
constraints = {
|
|
110
|
+
platform: {
|
|
111
|
+
any: true
|
|
112
|
+
}
|
|
113
|
+
} satisfies ModuleConstraints
|
|
114
|
+
|
|
115
|
+
async check(_ctx: Katmer.TaskContext<KatmerProvider>): Promise<void> {}
|
|
116
|
+
async initialize(_ctx: Katmer.TaskContext<KatmerProvider>): Promise<void> {}
|
|
117
|
+
async cleanup(_ctx: Katmer.TaskContext<KatmerProvider>): Promise<void> {}
|
|
118
|
+
|
|
119
|
+
async execute(
|
|
120
|
+
ctx: Katmer.TaskContext<KatmerProvider>
|
|
121
|
+
): Promise<SetFactModuleResult> {
|
|
122
|
+
const { vars, render, deep } = this.#normalize(this.params)
|
|
123
|
+
|
|
124
|
+
const before = ctx.variables ?? {}
|
|
125
|
+
const produced: Record<string, unknown> = {}
|
|
126
|
+
|
|
127
|
+
// Render each value according to flags, then collect into `produced`.
|
|
128
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
129
|
+
produced[k] =
|
|
130
|
+
render ?
|
|
131
|
+
await evalIterative(v, {
|
|
132
|
+
scope: { ...before, ...produced },
|
|
133
|
+
deep: !!deep
|
|
134
|
+
})
|
|
135
|
+
: v
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Determine changed by comparing JSON representations of affected keys.
|
|
139
|
+
let changed = false
|
|
140
|
+
for (const [k, nextVal] of Object.entries(produced)) {
|
|
141
|
+
const prevVal = (before as any)[k]
|
|
142
|
+
if (JSON.stringify(prevVal) !== JSON.stringify(nextVal)) {
|
|
143
|
+
changed = true
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Merge into ctx.variables
|
|
148
|
+
ctx.variables = toMerged(before, produced) as any
|
|
149
|
+
|
|
150
|
+
// Optional logging via ctx.logger
|
|
151
|
+
ctx.logger?.debug?.({ msg: "set_fact applied", facts: produced })
|
|
152
|
+
|
|
153
|
+
return { changed, facts: produced }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#normalize(p: SetFactModuleOptions | undefined): {
|
|
157
|
+
vars: Record<string, unknown>
|
|
158
|
+
render: boolean
|
|
159
|
+
deep: boolean
|
|
160
|
+
} {
|
|
161
|
+
if (!p) return { vars: {}, render: true, deep: false }
|
|
162
|
+
if (typeof p === "object" && "vars" in p) {
|
|
163
|
+
return {
|
|
164
|
+
vars: (p.vars || {}) as Record<string, unknown>,
|
|
165
|
+
render: p.render !== false,
|
|
166
|
+
deep: !!p.deep
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return { vars: p as Record<string, unknown>, render: true, deep: false }
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ModuleCommonReturn,
|
|
3
|
+
type ModuleConstraints
|
|
4
|
+
} from "../interfaces/module.interface"
|
|
5
|
+
import type { Katmer } from "../interfaces/task.interface"
|
|
6
|
+
import type { SSHProvider } from "../providers/ssh/ssh.provider"
|
|
7
|
+
import { KatmerModule } from "../module"
|
|
8
|
+
|
|
9
|
+
declare module "../interfaces/task.interface" {
|
|
10
|
+
export namespace Katmer {
|
|
11
|
+
export interface TaskActions {
|
|
12
|
+
systemd_service?: SystemdServiceModuleOptions
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Manage systemd units (start/stop/restart/reload/enable/disable/mask/unmask/daemon-reload).
|
|
18
|
+
*
|
|
19
|
+
* @remarks
|
|
20
|
+
* - Requires systemd on the target (systemctl must be available).
|
|
21
|
+
* - Operations are idempotent where feasible by checking current unit state.
|
|
22
|
+
*
|
|
23
|
+
* @examples
|
|
24
|
+
* ```yaml
|
|
25
|
+
* - name: Start and enable a service
|
|
26
|
+
* systemd_service:
|
|
27
|
+
* name: nginx
|
|
28
|
+
* state: started
|
|
29
|
+
* enabled: true
|
|
30
|
+
*
|
|
31
|
+
* - name: Restart with daemon-reload
|
|
32
|
+
* systemd_service:
|
|
33
|
+
* name: myapp.service
|
|
34
|
+
* daemon_reload: true
|
|
35
|
+
* state: restarted
|
|
36
|
+
*
|
|
37
|
+
* - name: Stop and disable a timer
|
|
38
|
+
* systemd_service:
|
|
39
|
+
* name: myjob.timer
|
|
40
|
+
* state: stopped
|
|
41
|
+
* enabled: false
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export class SystemdServiceModule extends KatmerModule<
|
|
45
|
+
SystemdServiceModuleOptions,
|
|
46
|
+
SystemdServiceModuleResult,
|
|
47
|
+
SSHProvider
|
|
48
|
+
> {
|
|
49
|
+
static name = "systemd_service" as const
|
|
50
|
+
|
|
51
|
+
constraints = {
|
|
52
|
+
platform: {
|
|
53
|
+
linux: {
|
|
54
|
+
requireRoot: true, // system scope typically needs root
|
|
55
|
+
binaries: [
|
|
56
|
+
// Parse "systemd 245 (...)" from `systemctl --version`
|
|
57
|
+
{
|
|
58
|
+
cmd: "systemctl",
|
|
59
|
+
args: ["--version"],
|
|
60
|
+
versionRegex: /systemd\s+(\d+)/,
|
|
61
|
+
range: ">=219"
|
|
62
|
+
}
|
|
63
|
+
],
|
|
64
|
+
// Shorthand strings are normalized to { name, range }
|
|
65
|
+
packages: ["systemd@>=219"],
|
|
66
|
+
// Turn off known non-systemd distro
|
|
67
|
+
distro: {
|
|
68
|
+
alpine: false
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} satisfies ModuleConstraints
|
|
73
|
+
|
|
74
|
+
async check(ctx: Katmer.TaskContext<SSHProvider>): Promise<void> {
|
|
75
|
+
const { stdout } = await ctx.exec(
|
|
76
|
+
`command -v systemctl >/dev/null 2>&1; echo $?`
|
|
77
|
+
)
|
|
78
|
+
if (stdout.trim() !== "0") {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"systemctl not found; target does not appear to be using systemd"
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
if (!this.params?.name || !String(this.params.name).trim()) {
|
|
84
|
+
throw new Error("'name' is required (unit name, e.g., nginx.service)")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async initialize(_ctx: Katmer.TaskContext<SSHProvider>): Promise<void> {}
|
|
89
|
+
|
|
90
|
+
async cleanup(_ctx: Katmer.TaskContext<SSHProvider>): Promise<void> {}
|
|
91
|
+
|
|
92
|
+
async execute(
|
|
93
|
+
ctx: Katmer.TaskContext<SSHProvider>
|
|
94
|
+
): Promise<SystemdServiceModuleResult> {
|
|
95
|
+
const {
|
|
96
|
+
name,
|
|
97
|
+
state,
|
|
98
|
+
enabled,
|
|
99
|
+
masked,
|
|
100
|
+
daemon_reload,
|
|
101
|
+
scope = "system", // or "user"
|
|
102
|
+
no_block = false
|
|
103
|
+
} = this.params
|
|
104
|
+
|
|
105
|
+
const unit = String(name).trim()
|
|
106
|
+
const scopeFlag = scope === "user" ? "--user" : ""
|
|
107
|
+
|
|
108
|
+
// helper
|
|
109
|
+
const ok = async (cmd: string) => {
|
|
110
|
+
const r = await ctx.exec(cmd)
|
|
111
|
+
return { code: r.code, out: r.stdout.trim(), err: r.stderr.trim() }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// daemon-reload first (commonly desired before actions)
|
|
115
|
+
let changed = false
|
|
116
|
+
if (daemon_reload) {
|
|
117
|
+
const r = await ok(`systemctl ${scopeFlag} daemon-reload`)
|
|
118
|
+
if (r.code !== 0) {
|
|
119
|
+
throw {
|
|
120
|
+
changed,
|
|
121
|
+
msg: r.err || r.out || "daemon-reload failed"
|
|
122
|
+
} as SystemdServiceModuleResult
|
|
123
|
+
}
|
|
124
|
+
changed = true
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Query current status/idempotency anchors
|
|
128
|
+
const isActive = await this.getIsActive(ctx, unit, scopeFlag)
|
|
129
|
+
const isEnabled = await this.getIsEnabled(ctx, unit, scopeFlag)
|
|
130
|
+
const isMasked = await this.getIsMasked(ctx, unit, scopeFlag)
|
|
131
|
+
|
|
132
|
+
// mask/unmask if requested explicitly
|
|
133
|
+
if (typeof masked === "boolean") {
|
|
134
|
+
if (masked && !isMasked) {
|
|
135
|
+
const r = await ok(
|
|
136
|
+
`systemctl ${scopeFlag} mask ${this.blockFlag(no_block)} ${q(unit)}`
|
|
137
|
+
)
|
|
138
|
+
if (r.code !== 0) {
|
|
139
|
+
throw {
|
|
140
|
+
changed,
|
|
141
|
+
msg: r.err || r.out || "mask failed"
|
|
142
|
+
} as SystemdServiceModuleResult
|
|
143
|
+
}
|
|
144
|
+
changed = true
|
|
145
|
+
} else if (!masked && isMasked) {
|
|
146
|
+
const r = await ok(
|
|
147
|
+
`systemctl ${scopeFlag} unmask ${this.blockFlag(no_block)} ${q(unit)}`
|
|
148
|
+
)
|
|
149
|
+
if (r.code !== 0) {
|
|
150
|
+
throw {
|
|
151
|
+
changed,
|
|
152
|
+
msg: r.err || r.out || "unmask failed"
|
|
153
|
+
} as SystemdServiceModuleResult
|
|
154
|
+
}
|
|
155
|
+
changed = true
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// enable/disable if requested
|
|
160
|
+
if (typeof enabled === "boolean") {
|
|
161
|
+
if (enabled && !isEnabled) {
|
|
162
|
+
const r = await ok(
|
|
163
|
+
`systemctl ${scopeFlag} enable ${this.blockFlag(no_block)} ${q(unit)}`
|
|
164
|
+
)
|
|
165
|
+
if (r.code !== 0) {
|
|
166
|
+
throw {
|
|
167
|
+
changed,
|
|
168
|
+
msg: r.err || r.out || "enable failed"
|
|
169
|
+
} as SystemdServiceModuleResult
|
|
170
|
+
}
|
|
171
|
+
changed = true
|
|
172
|
+
} else if (!enabled && isEnabled) {
|
|
173
|
+
const r = await ok(
|
|
174
|
+
`systemctl ${scopeFlag} disable ${this.blockFlag(no_block)} ${q(unit)}`
|
|
175
|
+
)
|
|
176
|
+
if (r.code !== 0) {
|
|
177
|
+
throw {
|
|
178
|
+
changed,
|
|
179
|
+
msg: r.err || r.out || "disable failed"
|
|
180
|
+
} as SystemdServiceModuleResult
|
|
181
|
+
}
|
|
182
|
+
changed = true
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// state transitions
|
|
187
|
+
if (state) {
|
|
188
|
+
if (state === "started" && !isActive) {
|
|
189
|
+
const r = await ok(
|
|
190
|
+
`systemctl ${scopeFlag} start ${this.blockFlag(no_block)} ${q(unit)}`
|
|
191
|
+
)
|
|
192
|
+
if (r.code !== 0) {
|
|
193
|
+
throw {
|
|
194
|
+
changed,
|
|
195
|
+
msg: r.err || r.out || "start failed"
|
|
196
|
+
} as SystemdServiceModuleResult
|
|
197
|
+
}
|
|
198
|
+
changed = true
|
|
199
|
+
} else if (state === "stopped" && isActive) {
|
|
200
|
+
const r = await ok(
|
|
201
|
+
`systemctl ${scopeFlag} stop ${this.blockFlag(no_block)} ${q(unit)}`
|
|
202
|
+
)
|
|
203
|
+
if (r.code !== 0) {
|
|
204
|
+
throw {
|
|
205
|
+
changed,
|
|
206
|
+
msg: r.err || r.out || "stop failed"
|
|
207
|
+
} as SystemdServiceModuleResult
|
|
208
|
+
}
|
|
209
|
+
changed = true
|
|
210
|
+
} else if (state === "restarted") {
|
|
211
|
+
const r = await ok(
|
|
212
|
+
`systemctl ${scopeFlag} restart ${this.blockFlag(no_block)} ${q(unit)}`
|
|
213
|
+
)
|
|
214
|
+
if (r.code !== 0) {
|
|
215
|
+
throw {
|
|
216
|
+
changed,
|
|
217
|
+
msg: r.err || r.out || "restart failed"
|
|
218
|
+
} as SystemdServiceModuleResult
|
|
219
|
+
}
|
|
220
|
+
changed = true
|
|
221
|
+
} else if (state === "reloaded") {
|
|
222
|
+
const r = await ok(
|
|
223
|
+
`systemctl ${scopeFlag} reload ${this.blockFlag(no_block)} ${q(unit)}`
|
|
224
|
+
)
|
|
225
|
+
if (r.code !== 0) {
|
|
226
|
+
throw {
|
|
227
|
+
changed,
|
|
228
|
+
msg: r.err || r.out || "reload failed"
|
|
229
|
+
} as SystemdServiceModuleResult
|
|
230
|
+
}
|
|
231
|
+
changed = true
|
|
232
|
+
} else if (state === "paused" || state === "unpaused") {
|
|
233
|
+
// No direct systemctl verb; map paused -> stop, unpaused -> start (best-effort)
|
|
234
|
+
if (state === "paused" && isActive) {
|
|
235
|
+
const r = await ok(
|
|
236
|
+
`systemctl ${scopeFlag} stop ${this.blockFlag(no_block)} ${q(unit)}`
|
|
237
|
+
)
|
|
238
|
+
if (r.code !== 0) {
|
|
239
|
+
throw {
|
|
240
|
+
changed,
|
|
241
|
+
msg: r.err || r.out || "pause(stop) failed"
|
|
242
|
+
} as SystemdServiceModuleResult
|
|
243
|
+
}
|
|
244
|
+
changed = true
|
|
245
|
+
}
|
|
246
|
+
if (state === "unpaused" && !isActive) {
|
|
247
|
+
const r = await ok(
|
|
248
|
+
`systemctl ${scopeFlag} start ${this.blockFlag(no_block)} ${q(unit)}`
|
|
249
|
+
)
|
|
250
|
+
if (r.code !== 0) {
|
|
251
|
+
throw {
|
|
252
|
+
changed,
|
|
253
|
+
msg: r.err || r.out || "unpause(start) failed"
|
|
254
|
+
} as SystemdServiceModuleResult
|
|
255
|
+
}
|
|
256
|
+
changed = true
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Re-query for result
|
|
262
|
+
const finalActive = await this.getIsActive(ctx, unit, scopeFlag)
|
|
263
|
+
const finalEnabled = await this.getIsEnabled(ctx, unit, scopeFlag)
|
|
264
|
+
const finalMasked = await this.getIsMasked(ctx, unit, scopeFlag)
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
changed,
|
|
268
|
+
status: {
|
|
269
|
+
name: unit,
|
|
270
|
+
active: finalActive,
|
|
271
|
+
enabled: finalEnabled,
|
|
272
|
+
masked: finalMasked,
|
|
273
|
+
scope
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
private blockFlag(no_block?: boolean) {
|
|
279
|
+
// systemctl is synchronous by default; when no_block is true, add --no-block
|
|
280
|
+
return no_block ? "--no-block" : ""
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private async getIsActive(
|
|
284
|
+
ctx: Katmer.TaskContext<SSHProvider>,
|
|
285
|
+
unit: string,
|
|
286
|
+
scopeFlag: string
|
|
287
|
+
) {
|
|
288
|
+
const r = await ctx.exec(
|
|
289
|
+
`systemctl ${scopeFlag} is-active ${q(unit)} || true`
|
|
290
|
+
)
|
|
291
|
+
return r.stdout.trim() === "active"
|
|
292
|
+
}
|
|
293
|
+
private async getIsEnabled(
|
|
294
|
+
ctx: Katmer.TaskContext<SSHProvider>,
|
|
295
|
+
unit: string,
|
|
296
|
+
scopeFlag: string
|
|
297
|
+
) {
|
|
298
|
+
const r = await ctx.exec(
|
|
299
|
+
`systemctl ${scopeFlag} is-enabled ${q(unit)} || true`
|
|
300
|
+
)
|
|
301
|
+
const s = r.stdout.trim()
|
|
302
|
+
return s === "enabled" || s === "static" || s === "indirect"
|
|
303
|
+
}
|
|
304
|
+
private async getIsMasked(
|
|
305
|
+
ctx: Katmer.TaskContext<SSHProvider>,
|
|
306
|
+
unit: string,
|
|
307
|
+
scopeFlag: string
|
|
308
|
+
) {
|
|
309
|
+
const r = await ctx.exec(
|
|
310
|
+
`systemctl ${scopeFlag} is-enabled ${q(unit)} || true`
|
|
311
|
+
)
|
|
312
|
+
return r.stdout.trim() === "masked"
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Options for systemd_service module.
|
|
318
|
+
* @public
|
|
319
|
+
*/
|
|
320
|
+
export interface SystemdServiceModuleOptions {
|
|
321
|
+
/**
|
|
322
|
+
* Unit name, e.g., "nginx.service" (".service" suffix optional).
|
|
323
|
+
*/
|
|
324
|
+
name: string
|
|
325
|
+
/**
|
|
326
|
+
* Desired unit state.
|
|
327
|
+
*/
|
|
328
|
+
state?:
|
|
329
|
+
| "started"
|
|
330
|
+
| "stopped"
|
|
331
|
+
| "restarted"
|
|
332
|
+
| "reloaded"
|
|
333
|
+
| "paused"
|
|
334
|
+
| "unpaused"
|
|
335
|
+
/**
|
|
336
|
+
* Enable or disable unit at boot.
|
|
337
|
+
*/
|
|
338
|
+
enabled?: boolean
|
|
339
|
+
/**
|
|
340
|
+
* Mask or unmask the unit.
|
|
341
|
+
*/
|
|
342
|
+
masked?: boolean
|
|
343
|
+
/**
|
|
344
|
+
* Run `systemctl daemon-reload` before actions.
|
|
345
|
+
*/
|
|
346
|
+
daemon_reload?: boolean
|
|
347
|
+
/**
|
|
348
|
+
* Target scope (system/user). Default: system.
|
|
349
|
+
*/
|
|
350
|
+
scope?: "system" | "user"
|
|
351
|
+
/**
|
|
352
|
+
* Use --no-block for start/stop/restart/reload/enable/disable/mask/unmask.
|
|
353
|
+
*/
|
|
354
|
+
no_block?: boolean
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Result for systemd_service module.
|
|
359
|
+
* @public
|
|
360
|
+
*/
|
|
361
|
+
export interface SystemdServiceModuleResult extends ModuleCommonReturn {
|
|
362
|
+
status: {
|
|
363
|
+
name: string
|
|
364
|
+
active: boolean
|
|
365
|
+
enabled: boolean
|
|
366
|
+
masked: boolean
|
|
367
|
+
scope: "system" | "user" | string
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function q(s: string) {
|
|
372
|
+
return JSON.stringify(s)
|
|
373
|
+
}
|