about summary refs log tree commit diff
path: root/items
diff options
context:
space:
mode:
authorRobert Günzler <r@gnzler.io>2025-09-07 17:32:13 +0200
committerRobert Günzler <r@gnzler.io>2025-09-07 17:42:42 +0200
commit38be6c13f76e893cf47636257071b440dec0c5b2 (patch)
treef7ded3235a7e3362cbd1ed3d91523968fe0faf90 /items
initial commit
Signed-off-by: Robert Günzler <r@gnzler.io>
Diffstat (limited to '')
-rw-r--r--items/svc_openrc2.py126
-rw-r--r--items/svc_s6rc.py140
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)
+