diff options
| author | Robert Günzler <r@gnzler.io> | 2025-09-07 17:32:13 +0200 |
|---|---|---|
| committer | Robert Günzler <r@gnzler.io> | 2025-09-07 17:42:42 +0200 |
| commit | 38be6c13f76e893cf47636257071b440dec0c5b2 (patch) | |
| tree | f7ded3235a7e3362cbd1ed3d91523968fe0faf90 /items | |
initial commit
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to 'items')
| -rw-r--r-- | items/svc_openrc2.py | 126 | ||||
| -rw-r--r-- | items/svc_s6rc.py | 140 |
2 files changed, 266 insertions, 0 deletions
diff --git a/items/svc_openrc2.py b/items/svc_openrc2.py new file mode 100644 index 0000000..5b74f61 --- /dev/null +++ b/items/svc_openrc2.py @@ -0,0 +1,126 @@ +from shlex import quote + +from bundlewrap.exceptions import BundleError +from bundlewrap.items import Item +from bundlewrap.utils.text import mark_for_translation as _ + + +def svc_start(node, svcname): + return node.run(f"rc-service {quote(svcname)} start", may_fail=True) + + +def svc_running(node, svcname): + result = node.run(f"rc-service {quote(svcname)} status", may_fail=True) + return result.return_code == 0 and "started" in result.stdout_text + + +def svc_stop(node, svcname): + return node.run(f"rc-service {quote(svcname)} stop", may_fail=True) + + +def svc_enable(node, svcname, runlevel): + return node.run(f"rc-update add {quote(svcname)} {quote(runlevel)}", may_fail=True) + + +def svc_enabled(node, svcname, runlevel): + result = node.run( + f"rc-update show {quote(runlevel)} | grep -w {quote(svcname)}", + may_fail=True, + ) + return result.return_code == 0 and svcname in result.stdout_text + + +def svc_runlevel(node, svcname): + result = node.run( + f"rc-update show --all | grep -w {quote(svcname)} | cut -d \\| -f 2", + may_fail=True, + ) + return result.stdout_text.strip() if result.return_code == 0 else None + + +def svc_disable(node, svcname, runlevel): + return node.run(f"rc-update del {quote(svcname)} {quote(runlevel)}", may_fail=True) + + +class SvcOpenRC2(Item): + """ + A service managed by OpenRC init scripts. + """ + + BUNDLE_ATTRIBUTE_NAME = "svc_openrc2" + ITEM_ATTRIBUTES = { + "running": True, + "enabled": True, + "runlevel": "default", + } + ITEM_TYPE_NAME = "svc_openrc2" + + def __repr__(self): + return "<SvcOpenRC name:{} runlevel:{} enabled:{} running:{}>".format( + self.name, + self.attributes["runlevel"], + self.attributes["enabled"], + self.attributes["running"], + ) + + def fix(self, status): + if "enabled" in status.keys_to_fix: + if self.attributes["enabled"]: + svc_enable(self.node, self.name, self.attributes["runlevel"]) + else: + svc_disable(self.node, self.name, self.attributes["runlevel"]) + + if "running" in status.keys_to_fix: + if self.attributes["running"]: + svc_start(self.node, self.name) + else: + svc_stop(self.node, self.name) + + if "runlevel" in status.keys_to_fix: + if status.sdict["runlevel"]: + svc_disable(self.node, self.name, status.sdict["runlevel"]) + svc_enable(self.node, self.name, self.attributes["runlevel"]) + + def get_canned_actions(self): + return { + "stop": { + "command": f"rc-service {self.name} stop", + "needed_by": {self.id}, + }, + "restart": { + "command": f"rc-service {self.name} restart", + "needs": {self.id}, + }, + "reload": { + "command": f"rc-service {self.name} reload".format(self.name), + "needs": { + # make sure we don't reload and restart simultaneously + f"{self.id}:restart", + # with only the dep on restart, we might still end + # up reloading if the service itself is skipped + # because the restart action has cascade_skip False + self.id, + }, + }, + } + + def sdict(self): + return { + "enabled": svc_enabled(self.node, self.name, self.attributes["runlevel"]), + "running": svc_running(self.node, self.name), + "runlevel": svc_runlevel(self.node, self.name), + } + + @classmethod + def validate_attributes(cls, bundle, item_id, attributes): + for attribute in ("enabled", "running"): + if attributes.get(attribute, None) not in (True, False, None): + raise BundleError( + _( + "expected boolean or None for '{attribute}' on {item} in bundle '{bundle}'" + ).format( + attribute=attribute, + bundle=bundle.name, + item=item_id, + ) + ) diff --git a/items/svc_s6rc.py b/items/svc_s6rc.py new file mode 100644 index 0000000..429c04a --- /dev/null +++ b/items/svc_s6rc.py @@ -0,0 +1,140 @@ +from shlex import quote +from pathlib import Path + +from bundlewrap.items import Item + +S6RC_ROOT = "/etc/s6-rc" + +def generate_request_path(template, service) -> str: + return Path(S6RC_ROOT, "generate", f"{template}.{service}").as_posix() + +def service_dependency_path(bundle, service) -> str: + return Path(S6RC_ROOT, "services", bundle, "contents.d", service).as_posix() + +def have_generate_template(node, template) -> bool: + template_path = Path(S6RC_ROOT, "templates", template) + try: + result = node.run( + f"test -d {quote(template_path.as_posix())}", + ) + return result.return_code == 0 + except: + return False + +def is_service_generated(node, template, service) -> bool: + try: + result = node.run( + f"test -f {quote(generate_request_path(template, service))}", + ) + return result.return_code == 0 + except: + return False + +def generate_service(node, template, service) -> bool: + """ + This doesn't really generate the service from the template. Rather it + requests the s6-rc-generate target to do it on boot. + """ + result = node.run( + f"touch {quote(generate_request_path(template, service))}", + ) + return result.return_code == 0 + +def is_service_registered(node, bundle, service) -> bool: + try: + result = node.run( + f"test -f {quote(service_dependency_path(bundle, service))}", + ) + return result.return_code == 0 + except: + return False + +def register_service(node, bundle, service) -> bool: + result = node.run( + f"touch {quote(service_dependency_path(bundle, service))}", + ) + return result.return_code == 0 + +def is_service_running(node, service) -> bool: + try: + result = node.run( + f"s6-rc -v2 -bt 30000 -a list | grep -q {quote(service)}", + ) + return result.return_code == 0 + except: + return False + +def run_service(node, service) -> bool: + result = node.run( + f"s6-rc -v2 -bt 30000 start {quote(service)}", + ) + return result.return_code == 0 + +class SvcS6RC(Item): + """ + A service managed by s6-rc. + """ + + BUNDLE_ATTRIBUTE_NAME = "svc_s6rc" + ITEM_ATTRIBUTES = { + "bundle": "default", + "generate": None, + "running": True, + } + ITEM_TYPE_NAME = "svc_s6rc" + REQUIRED_ATTRIBUTES = [] + + def __repr__(self): + return "<SvcS6RC name:{} running:{} bundle:{} generate:{}>".format( + self.name, + self.attributes["running"], + self.attributes["bundle"], + self.attributes["generate"], + ) + + def get_canned_actions(self): + return { + "stop": { + "command": f"s6-rc -v2 -b -t 30000 stop {quote(self.name)}", + "needed_by": {self.id}, + }, + "restart": { + "command": f"s6-rc -v2 -b -t 30000 stop {quote(self.name)} && s6-rc -v2 -b -t 30000 start {quote(self.name)}", + "needs": {self.id}, + }, + } + + def cdict(self): + return { + "running": self.attributes["running"], + "generated": self.attributes["generate"] != None, + "registered": self.attributes["bundle"] != None, + } + + def sdict(self): + return { + "running": is_service_running(self.node, self.name), + "generated": is_service_generated(self.node, + self.attributes["generate"], + self.name) if + self.attributes["generate"] != None else False, + "registered": is_service_registered(self.node,self.attributes["bundle"], self.name), + } + + def fix(self, status): + if status.correct: + return + + if self.attributes["generate"]: + if not have_generate_template(self.node, self.attributes["generate"]): + raise Exception(f"No template provides `{self.attributes["generate"]}`") + + if status.cdict["generated"] != status.sdict["generated"]: + generate_service(self.node, self.attributes["generate"], self.name) + + if status.cdict["registered"] != status.sdict["registered"]: + register_service(self.node, self.attributes["bundle"], self.name) + + if status.cdict["running"] != status.sdict["running"]: + run_service(self.node, self.name) + |