Repository: osbuild/osbuild
Pull request #1891 (2024-09-18 17:12:55)

Update snapshots to 20240918
Results of the snapshot jobs:
Job(s) succeeded: 202
Job(s) failed: 1

If these are false, rebuild the enumerate cache manually:
Enumerate cache job succeeded: true
Check snapshot succeeded: true

Workflow run: https://github.com/osbuild/rpmrepo/actions/runs/10922958520



Patch:
diff --git a/Schutzfile b/Schutzfile
index 6b629a2e..6c1e4f8c 100644
--- a/Schutzfile
+++ b/Schutzfile
@@ -19,7 +19,7 @@
           {
             "title": "fedora-updates",
             "name": "fedora-updates",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f39/f39-x86_64-updates-released-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f39/f39-x86_64-updates-released-20240918"
           }
         ],
         "aarch64": [
@@ -31,7 +31,7 @@
           {
             "title": "fedora-updates",
             "name": "fedora-updates",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f39/f39-aarch64-updates-released-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f39/f39-aarch64-updates-released-20240918"
           }
         ]
       }
@@ -50,7 +50,7 @@
           {
             "title": "fedora-updates",
             "name": "fedora-updates",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f40/f40-x86_64-updates-released-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f40/f40-x86_64-updates-released-20240918"
           }
         ],
         "aarch64": [
@@ -62,7 +62,7 @@
           {
             "title": "fedora-updates",
             "name": "fedora-updates",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f40/f40-aarch64-updates-released-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/f40/f40-aarch64-updates-released-20240918"
           }
         ]
       }
@@ -97,34 +97,34 @@
           {
             "title": "baseos",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-baseos-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-baseos-20240918"
           },
           {
             "title": "appstream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-appstream-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-appstream-20240918"
           },
           {
             "title": "crb",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-crb-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-x86_64-crb-20240918"
           }
         ],
         "aarch64": [
           {
             "title": "baseos",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-baseos-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-baseos-20240918"
           },
           {
             "title": "appstream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-appstream-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-appstream-20240918"
           },
           {
             "title": "crb",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-crb-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el9/cs9-aarch64-crb-20240918"
           }
         ]
       }
@@ -138,34 +138,34 @@
           {
             "title": "baseos",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-baseos-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-baseos-20240918"
           },
           {
             "title": "appstream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-appstream-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-appstream-20240918"
           },
           {
             "title": "crb",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-crb-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-x86_64-crb-20240918"
           }
         ],
         "aarch64": [
           {
             "title": "baseos",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-baseos-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-baseos-20240918"
           },
           {
             "title": "appstream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-appstream-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-appstream-20240918"
           },
           {
             "title": "crb",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-crb-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/public/el10/cs10-aarch64-crb-20240918"
           }
         ]
       }
@@ -179,34 +179,34 @@
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-BaseOS",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-baseos-n9.5-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-baseos-n9.5-20240918"
           },
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-AppStream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.5-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-appstream-n9.5-20240918"
           },
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-CRB",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-crb-n9.5-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-x86_64-crb-n9.5-20240918"
           }
         ],
         "aarch64": [
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-BaseOS",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-baseos-n9.5-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-baseos-n9.5-20240918"
           },
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-AppStream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.5-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-appstream-n9.5-20240918"
           },
           {
             "title": "RHEL-9-RPMREPO-NIGHTLY-CRB",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-crb-n9.5-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el9/el9-aarch64-crb-n9.5-20240918"
           }
         ]
       }
@@ -220,34 +220,34 @@
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-BaseOS",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-baseos-n10.0-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-baseos-n10.0-20240918"
           },
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-AppStream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-appstream-n10.0-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-appstream-n10.0-20240918"
           },
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-CRB",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-crb-n10.0-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-x86_64-crb-n10.0-20240918"
           }
         ],
         "aarch64": [
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-BaseOS",
             "name": "baseos",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-baseos-n10.0-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-baseos-n10.0-20240918"
           },
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-AppStream",
             "name": "appstream",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-appstream-n10.0-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-appstream-n10.0-20240918"
           },
           {
             "title": "RHEL-10-RPMREPO-NIGHTLY-CRB",
             "name": "crb",
-            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-crb-n10.0-20240901"
+            "baseurl": "https://rpmrepo.osbuild.org/v2/mirror/rhvpn/el10/el10-aarch64-crb-n10.0-20240918"
           }
         ]
       }
diff --git a/stages/org.osbuild.containers.unit.create b/stages/org.osbuild.containers.unit.create
new file mode 100755
index 00000000..23914c02
--- /dev/null
+++ b/stages/org.osbuild.containers.unit.create
@@ -0,0 +1,61 @@
+#!/usr/bin/python3
+import configparser
+import sys
+
+import osbuild.api
+
+
+def validate(filename, cfg):
+    # ensure the service name does not exceed maximum filename length
+    if len(filename) > 255:
+        raise ValueError(f"Error: the {filename} unit exceeds the maximum filename length.")
+
+    # Filename extension must match the config:
+    #   .service requires a Service section
+    #   .mount requires a Mount section
+    if filename.endswith(".container") and "Container" not in cfg:
+        raise ValueError(f"Error: {filename} unit requires Container section")
+    if filename.endswith(".volume") and "Volume" not in cfg:
+        raise ValueError(f"Error: {filename} unit requires Volume section")
+    if filename.endswith(".network") and "Network" not in cfg:
+        raise ValueError(f"Error: {filename} unit requires Network section")
+
+
+def main(tree, options):
+    filename = options["filename"]
+    cfg = options["config"]
+    validate(filename, cfg)
+
+    # We trick configparser into letting us write multiple instances of the same option by writing them as keys with no
+    # value, so we enable allow_no_value
+    config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
+    # prevent conversion of the option name to lowercase
+    config.optionxform = lambda option: option
+
+    for section, opts in cfg.items():
+        if not config.has_section(section):
+            config.add_section(section)
+        for option, value in opts.items():
+            if isinstance(value, list):
+                for v in value:
+                    if option == "Environment":
+                        # Option value becomes "KEY=VALUE" (quoted)
+                        v = '"' + v["key"] + "=" + str(v["value"]) + '"'
+                    config.set(section, str(option) + "=" + str(v))
+            else:
+                config.set(section, option, str(value))
+    persistent = options.get("unit-path", "usr")
+    systemd_dir = str()
+    if persistent == "usr":
+        systemd_dir = f"{tree}/usr/share/containers/systemd"
+    elif persistent == "etc":
+        systemd_dir = f"{tree}/etc/containers/systemd"
+
+    with open(f"{systemd_dir}/{filename}", "w", encoding="utf8") as f:
+        config.write(f, space_around_delimiters=False)
+
+
+if __name__ == '__main__':
+    args = osbuild.api.arguments()
+    r = main(args["tree"], args["options"])
+    sys.exit(r)
diff --git a/stages/org.osbuild.containers.unit.create.meta.json b/stages/org.osbuild.containers.unit.create.meta.json
new file mode 100644
index 00000000..1cbbfed0
--- /dev/null
+++ b/stages/org.osbuild.containers.unit.create.meta.json
@@ -0,0 +1,348 @@
+{
+  "summary": "Create a podman systemd unit file",
+  "description": [
+    "This stage allows to create Podman systemd (quadlet) unit files. The `filename` property",
+    "specifies the, '.service' or '.mount' file to be added. These names are",
+    "validated using the, same rules as specified by podman-systemd.unit(5) and they",
+    "must contain the, '.container', '.volume' or '.network' suffix (other types of unit files",
+    "are not supported). 'unit-path' determines determine the unit load path.",
+    "",
+    "The Unit configuration can currently specify the following subset",
+    "of options:",
+    "  - 'Unit' section",
+    "    - 'Description' - string",
+    "    - 'ConditionPathExists' - string",
+    "    - 'ConditionPathIsDirectory' - string",
+    "    - 'DefaultDependencies' - bool",
+    "    - 'Requires' - [strings]",
+    "    - 'Wants' - [strings]",
+    "    - 'After' - [strings]",
+    "    - 'Before' - [strings]",
+    "  - 'Service' section",
+    "    - 'Restart' - string",
+    "  - 'Container' section",
+    "    - 'Image' - string",
+    "    - 'Exec' - string",
+    "    - 'Volume' - [string]",
+    "    - 'User' - string",
+    "    - 'Group' - string",
+    "    - 'AddDevice' - string",
+    "    - 'Environment' - [object]",
+    "    - 'Network' - string",
+    "    - 'WorkingDir' - string",
+    "  - 'Volume' section",
+    "    - 'VolumeName' - string",
+    "    - 'Driver' - string",
+    "    - 'Image' - string",
+    "    - 'User' - string",
+    "    - 'Group' - string",
+    "  - 'Network' section",
+    "    - 'Gateway' - string",
+    "    - 'DNS' - string",
+    "    - 'IPRange' - string",
+    "    - 'Subnet' - string",
+    "    - 'Driver' - string",
+    "    - 'NetworkName' - string",
+    "  - 'Install' section",
+    "    - 'WantedBy' - [string]",
+    "    - 'RequiredBy' - [string]"
+  ],
+  "schema": {
+    "additionalProperties": false,
+    "required": [
+      "filename",
+      "config"
+    ],
+    "properties": {
+      "filename": {
+        "type": "string",
+        "pattern": "^[\\w:.\\\\-]+[@]{0,1}[\\w:.\\\\-]*\\.(container|volume|network)$"
+      },
+      "unit-path": {
+        "type": "string",
+        "enum": [
+          "usr",
+          "etc"
+        ],
+        "default": "usr",
+        "description": "Define the system load path"
+      },
+      "config": {
+        "additionalProperties": false,
+        "type": "object",
+        "oneOf": [
+          {
+            "required": [
+              "Unit",
+              "Container",
+              "Install"
+            ],
+            "not": {
+              "anyOf": [
+                {
+                  "required": [
+                    "Volume"
+                  ]
+                },
+                {
+                  "required": [
+                    "Network"
+                  ]
+                }
+              ]
+            }
+          },
+          {
+            "required": [
+              "Volume"
+            ],
+            "not": {
+              "anyOf": [
+                {
+                  "required": [
+                    "Container"
+                  ]
+                },
+                {
+                  "required": [
+                    "Network"
+                  ]
+                }
+              ]
+            }
+          },
+          {
+            "required": [
+              "Network"
+            ],
+            "not": {
+              "anyOf": [
+                {
+                  "required": [
+                    "Container"
+                  ]
+                },
+                {
+                  "required": [
+                    "Volume"
+                  ]
+                }
+              ]
+            }
+          }
+        ],
+        "description": "Configuration for a '.container' unit.",
+        "properties": {
+          "Unit": {
+            "additionalProperties": false,
+            "type": "object",
+            "description": "'Unit' configuration section of a unit file.",
+            "properties": {
+              "Description": {
+                "type": "string"
+              },
+              "Wants": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "After": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "Before": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "Requires": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "ConditionPathExists": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "ConditionPathIsDirectory": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "DefaultDependencies": {
+                "type": "boolean"
+              }
+            }
+          },
+          "Service": {
+            "additionalProperties": false,
+            "type": "object",
+            "description": "'Service' configuration section of a unit file.",
+            "properties": {
+              "Restart": {
+                "type": "string",
+                "enum": [
+                  "no",
+                  "on-success",
+                  "on-failure",
+                  "on-abnormal",
+                  "on-watchdog",
+                  "on-abort",
+                  "always"
+                ]
+              }
+            }
+          },
+          "Container": {
+            "additionalProperties": false,
+            "type": "object",
+            "description": "'Container' configuration section of a unit file.",
+            "required": [
+              "Image"
+            ],
+            "properties": {
+              "Environment": {
+                "type": "array",
+                "description": "Sets environment variables for executed process.",
+                "items": {
+                  "type": "object",
+                  "description": "Sets environment variables for executed process.",
+                  "additionalProperties": false,
+                  "properties": {
+                    "key": {
+                      "type": "string",
+                      "pattern": "^[A-Za-z_][A-Za-z0-9_]*"
+                    },
+                    "value": {
+                      "type": "string"
+                    }
+                  }
+                }
+              },
+              "Image": {
+                "description": "Container Image to use",
+                "type": "string"
+              },
+              "Exec": {
+                "description": "Command to execute in container",
+                "type": "string"
+              },
+              "Volume": {
+                "description": "Volumes to use",
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "User": {
+                "description": "Run as user",
+                "type": "string"
+              },
+              "Group": {
+                "description": "Run as group",
+                "type": "string"
+              },
+              "AddDevice": {
+                "description": "Add device to container",
+                "type": "string"
+              },
+              "Network": {
+                "description": "What network option to use",
+                "type": "string"
+              },
+              "WorkingDir": {
+                "description": "Working directory for initial process",
+                "type": "string"
+              }
+            }
+          },
+          "Volume": {
+            "additionalProperties": false,
+            "type": "object",
+            "description": "'Volume' configuration section of a unit file.",
+            "properties": {
+              "VolumeName": {
+                "description": "Override volume name",
+                "type": "string"
+              },
+              "Driver": {
+                "description": "What volume driver to use",
+                "type": "string"
+              },
+              "Image": {
+                "description": "Image to use if driver is image",
+                "type": "string"
+              },
+              "User": {
+                "description": "User to use as owner of the volume",
+                "type": "string"
+              },
+              "Group": {
+                "description": "Group to use as owner of the volume",
+                "type": "string"
+              }
+            }
+          },
+          "Network": {
+            "additionalProperties": false,
+            "type": "object",
+            "description": "'Network' configuration section of a unit file.",
+            "properties": {
+              "Gateway": {
+                "description": "Addres of gaterway",
+                "type": "boolean"
+              },
+              "DNS": {
+                "description": "Address of DNS server",
+                "type": "boolean"
+              },
+              "IPRange": {
+                "description": "Range to allocate IPs from",
+                "type": "boolean"
+              },
+              "Subnet": {
+                "description": "Subnet in CIDR notation",
+                "type": "boolean"
+              },
+              "Driver": {
+                "description": "What network driver to use",
+                "type": "boolean"
+              },
+              "NetworkName": {
+                "description": "Override network name",
+                "type": "boolean"
+              }
+            }
+          },
+          "Install": {
+            "additionalProperties": false,
+            "type": "object",
+            "description": "'Install' configuration section of a unit file.",
+            "properties": {
+              "WantedBy": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              },
+              "RequiredBy": {
+                "type": "array",
+                "items": {
+                  "type": "string"
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/stages/org.osbuild.systemd.unit b/stages/org.osbuild.systemd.unit
index 354c664e..c4512a7a 100755
--- a/stages/org.osbuild.systemd.unit
+++ b/stages/org.osbuild.systemd.unit
@@ -24,7 +24,7 @@ def main(tree, options):
 
     # We trick configparser into letting us write multiple instances of the same option by writing them as keys with no
     # value, so we enable allow_no_value
-    config = configparser.ConfigParser(allow_no_value=True)
+    config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
     # prevent conversion of the option name to lowercase
     config.optionxform = lambda option: option
 
diff --git a/stages/org.osbuild.systemd.unit.create b/stages/org.osbuild.systemd.unit.create
index 2c367f3f..625f62b8 100755
--- a/stages/org.osbuild.systemd.unit.create
+++ b/stages/org.osbuild.systemd.unit.create
@@ -17,6 +17,8 @@ def validate(filename, cfg):
         raise ValueError(f"Error: {filename} unit requires Service section")
     if filename.endswith(".mount") and "Mount" not in cfg:
         raise ValueError(f"Error: {filename} unit requires Mount section")
+    if filename.endswith(".socket") and "Socket" not in cfg:
+        raise ValueError(f"Error: {filename} unit requires Socket section")
 
 
 def main(tree, options):
@@ -26,7 +28,7 @@ def main(tree, options):
 
     # We trick configparser into letting us write multiple instances of the same option by writing them as keys with no
     # value, so we enable allow_no_value
-    config = configparser.ConfigParser(allow_no_value=True)
+    config = configparser.ConfigParser(allow_no_value=True, interpolation=None)
     # prevent conversion of the option name to lowercase
     config.optionxform = lambda option: option
 
diff --git a/stages/org.osbuild.systemd.unit.create.meta.json b/stages/org.osbuild.systemd.unit.create.meta.json
index 2446a8c0..254bc0fa 100644
--- a/stages/org.osbuild.systemd.unit.create.meta.json
+++ b/stages/org.osbuild.systemd.unit.create.meta.json
@@ -33,6 +33,19 @@
     "    - 'Where' - string",
     "    - 'Type' - string",
     "    - 'Options' - string",
+    "  - 'Socket' section",
+    "    - 'ListenStream' - string",
+    "    - 'ListenDatagram' - string",
+    "    - 'ListenSequentialPacket' - string",
+    "    - 'ListenFIFO' - string",
+    "    - 'SocketUser' - string",
+    "    - 'SocketGroup' - string",
+    "    - 'SocketMode' - string",
+    "    - 'DirectoryMode' - string",
+    "    - 'Accept' - bool",
+    "    - 'Service' - string",
+    "    - 'RuntimeDirectory' - string",
+    "    - 'RemoveOnStop' - bool",
     "  - 'Install' section",
     "    - 'WantedBy' - [string]",
     "    - 'RequiredBy' - [string]"
@@ -46,7 +59,7 @@
     "properties": {
       "filename": {
         "type": "string",
-        "pattern": "^[\\w:.\\\\-]+[@]{0,1}[\\w:.\\\\-]*\\.(service|mount)$"
+        "pattern": "^[\\w:.\\\\-]+[@]{0,1}[\\w:.\\\\-]*\\.(service|mount|socket)$"
       },
       "unit-type": {
         "type": "string",
@@ -77,8 +90,36 @@
               "Install"
             ],
             "not": {
-              "required": [
-                "Mount"
+              "anyOf": [
+                {
+                  "required": [
+                    "Mount"
+                  ]
+                },
+                {
+                  "required": [
+                    "Socket"
+                  ]
+                }
+              ]
+            }
+          },
+          {
+            "required": [
+              "Socket"
+            ],
+            "not": {
+              "anyOf": [
+                {
+                  "required": [
+                    "Mount"
+                  ]
+                },
+                {
+                  "required": [
+                    "Service"
+                  ]
+                }
               ]
             }
           },
@@ -87,8 +128,17 @@
               "Mount"
             ],
             "not": {
-              "required": [
-                "Service"
+              "anyOf": [
+                {
+                  "required": [
+                    "Service"
+                  ]
+                },
+                {
+                  "required": [
+                    "Socket"
+                  ]
+                }
               ]
             }
           }
@@ -236,6 +286,61 @@
               }
             }
           },
+          "Socket": {
+            "additionalProperties": false,
+            "type": "object",
+            "description": "'Socket' configuration section of a unit file.",
+            "properties": {
+              "Service": {
+                "description": "",
+                "type": "string"
+              },
+              "ListenStream": {
+                "description": "",
+                "type": "string"
+              },
+              "ListenDatagram": {
+                "description": "",
+                "type": "string"
+              },
+              "ListenSequentialPacket": {
+                "description": "",
+                "type": "string"
+              },
+              "ListenFifo": {
+                "description": "",
+                "type": "string"
+              },
+              "SocketUser": {
+                "description": "",
+                "type": "string"
+              },
+              "SocketGroup": {
+                "description": "",
+                "type": "string"
+              },
+              "SocketMode": {
+                "description": "",
+                "type": "string"
+              },
+              "DirectoryMode": {
+                "description": "",
+                "type": "string"
+              },
+              "Accept": {
+                "description": "",
+                "type": "boolean"
+              },
+              "RuntimeDirectory": {
+                "description": "",
+                "type": "string"
+              },
+              "RemoveOnStop": {
+                "description": "",
+                "type": "boolean"
+              }
+            }
+          },
           "Install": {
             "additionalProperties": false,
             "type": "object",
diff --git a/tools/osbuild-mpp b/tools/osbuild-mpp
index 0252eac6..eb610be2 100755
--- a/tools/osbuild-mpp
+++ b/tools/osbuild-mpp
@@ -1608,13 +1608,15 @@ class ManifestFileV2(ManifestFile):
                 f, _ = self.find_and_open_file(path, [], mode="rb", encoding=None)
                 with f:
                     data = f.read()
+                checksum = hashlib.sha256(data).hexdigest()
             elif url:
                 response = urllib.request.urlopen(url)
-                data = response.fp.read()
+                h = hashlib.file_digest(response.fp, 'sha256')
+                checksum = h.hexdigest()
             else:
                 data = bytes(text, "utf-8")
+                checksum = hashlib.sha256(data).hexdigest()
 
-            checksum = hashlib.sha256(data).hexdigest()
             digest = "sha256:" + checksum
 
             if url:
@@ -1622,6 +1624,7 @@ class ManifestFileV2(ManifestFile):
                 items = element_enter(source, "items", {})
                 items[digest] = url
             else:
+                assert data is not None
                 encoded = base64.b64encode(data).decode("utf-8")
                 source = element_enter(self.sources, "org.osbuild.inline", {})
                 items = element_enter(source, "items", {})


Test patch:
diff --git a/stages/test/test_containers_unit_create.py b/stages/test/test_containers_unit_create.py
new file mode 100644
index 00000000..6e0782b6
--- /dev/null
+++ b/stages/test/test_containers_unit_create.py
@@ -0,0 +1,208 @@
+#!/usr/bin/python3
+
+import os
+import textwrap
+
+import pytest
+
+from osbuild import testutil
+
+STAGE_NAME = "org.osbuild.containers.unit.create"
+
+
+@pytest.mark.parametrize("test_data,expected_err", [
+    # good
+    (
+        {
+            "filename": "foo.container",
+            "config": {
+                "Unit": {},
+                "Service": {},
+                "Container": {
+                    "Image": "img"
+                },
+                "Install": {},
+            },
+        },
+        "",
+    ),
+    (
+        {
+            "filename": "foo.volume",
+            "config": {
+                "Unit": {},
+                "Volume": {},
+                "Install": {},
+            },
+        },
+        "",
+    ),
+    (
+        {
+            "filename": "foo.network",
+            "config": {
+                "Network": {},
+            },
+        },
+        "",
+    ),
+    # bad
+    ({"config": {"Unit": {}, "Container": {"Image": "img"}, "Install": {}}}, "'filename' is a required property"),
+    ({"filename": "foo.container"}, "'config' is a required property"),
+    ({"filename": "foo.container", "config": {"Container": {"Image": "img"}, "Install": {}}},
+     "{'Container': {'Image': 'img'}, 'Install': {}} is not valid under any of the given schemas"),
+    ({"filename": "foo.container", "config": {"Unit": {}, "Install": {}}},
+     "{'Unit': {}, 'Install': {}} is not valid under any of the given schemas"),
+    ({"filename": "foo.container", "config": {"Network": {}, "Volume": {}}},
+     "{'Network': {}, 'Volume': {}} is not valid under any of the given schemas")
+])
+@pytest.mark.parametrize("stage_schema", ["1"], indirect=True)
+def test_schema_validation(stage_schema, test_data, expected_err):
+    test_input = {
+        "name": STAGE_NAME,
+        "options": test_data,
+    }
+    res = stage_schema.validate(test_input)
+    if expected_err == "":
+        assert res.valid is True, f"err: {[e.as_dict() for e in res.errors]}"
+    else:
+        assert res.valid is False
+        testutil.assert_jsonschema_error_contains(res, expected_err, expected_num_errs=1)
+
+
+@pytest.mark.parametrize("test_data,expected_err", [
+    # good
+    (
+        {
+            "filename": "foo.service",
+            "config": {
+                "Unit": {},
+                "Service": {},
+                "Install": {},
+            },
+        },
+        "",
+    ),
+    (
+        {
+            "filename": "foo.network",
+            "config": {
+                "Unit": {},
+                "Network": {},
+                "Install": {},
+            },
+        },
+        "",
+    ),
+    (
+        {
+            "filename": "foo.volume",
+            "config": {
+                "Volume": {},
+            },
+        },
+        "",
+    ),
+    # bad
+    ({"filename": "something.container", "config": {"Unit": {}, "Volume": {}, "Install": {}}},
+     "Error: something.container unit requires Container section"),
+    ({"filename": "data-gifs-cats.volume", "config": {"Unit": {}, "Network": {}, "Install": {}}},
+     "Error: data-gifs-cats.volume unit requires Volume section"),
+])
+def test_name_config_match(tmp_path, stage_module, test_data, expected_err):
+    expected_unit_path = tmp_path / "usr/share/containers/systemd"
+    expected_unit_path.mkdir(parents=True, exist_ok=True)
+    try:
+        stage_module.main(tmp_path, test_data)
+    except ValueError as ve:
+        assert expected_err == str(ve)
+
+
+@pytest.mark.parametrize("unit_path,expected_prefix", [
+    ("usr", "usr/share/containers/systemd"),
+    ("etc", "etc/containers/systemd")
+])
+def test_systemd_unit_create(tmp_path, stage_module, unit_path, expected_prefix):
+    options = {
+        "filename": "create-directory.container",
+        "unit-path": unit_path,
+        "config": {
+            "Unit": {
+                "Description": "Create directory",
+                "DefaultDependencies": False,
+                "ConditionPathExists": [
+                    "|!/etc/myfile"
+                ],
+                "ConditionPathIsDirectory": [
+                    "|!/etc/mydir"
+                ],
+                # need to use real units otherwise the validation will fail
+                "Wants": [
+                    "basic.target",
+                    "sysinit.target",
+                ],
+                "Requires": [
+                    "paths.target",
+                    "sockets.target",
+                ],
+                "After": [
+                    "sysinit.target",
+                    "default.target",
+                ],
+                "Before": [
+                    "multi-user.target",
+                ],
+            },
+            "Container": {
+                "Image": "img",
+                "Environment": [
+                    {
+                        "key": "DEBUG",
+                        "value": "1",
+                    },
+                    {
+                        "key": "TRACE",
+                        "value": "1",
+                    },
+                ]
+            },
+            "Install": {
+                "WantedBy": [
+                    "local-fs.target"
+                ],
+                "RequiredBy": [
+                    "multi-user.target"
+                ]
+            }
+        }
+    }
+
+    expected_unit_path = tmp_path / expected_prefix / "create-directory.container"
+    expected_unit_path.parent.mkdir(parents=True, exist_ok=True)
+
+    stage_module.main(tmp_path, options)
+    assert os.path.exists(expected_unit_path)
+    assert expected_unit_path.read_text(encoding="utf-8") == textwrap.dedent("""\
+    [Unit]
+    Description=Create directory
+    DefaultDependencies=False
+    ConditionPathExists=|!/etc/myfile
+    ConditionPathIsDirectory=|!/etc/mydir
+    Wants=basic.target
+    Wants=sysinit.target
+    Requires=paths.target
+    Requires=sockets.target
+    After=sysinit.target
+    After=default.target
+    Before=multi-user.target
+
+    [Container]
+    Image=img
+    Environment="DEBUG=1"
+    Environment="TRACE=1"
+
+    [Install]
+    WantedBy=local-fs.target
+    RequiredBy=multi-user.target
+
+    """)