Repository: osbuild/osbuild
Pull request #59 (2019-08-02 17:38:56)

Add description() methods
We already allow loading from a description. This adds the opposite direction to export Pipelines, Stages, and Assemblers.


msehnout: ```
BUILDSTDERR: /var/tmp/rpm-tmp.wmmBsj: line 38: cd: osbuild-1: No such file or directory
```
this is weird, it works in all other PRs. Try it one more time before we start debugging RPM build again.

Patch:
diff --git a/.gitignore b/.gitignore
index 6b4b4b0e..2a8b3d47 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,9 @@
 *.rpm
 *.tar.gz
 *.egg-info
-output
+__pycache__
+
+/.osbuild
+/output
+
+/test/.vagrant
diff --git a/.travis.yml b/.travis.yml
index dcf6d97c..06789556 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -7,6 +7,8 @@ jobs:
     - name: pylint
       install: pip install pylint
       script: pylint osbuild osbuild-run assemblers/* stages/*
+    - name: unit-tests
+      script: python3 -m unittest test.test_osbuild
     - name: rpm
       before_install:
         - sudo apt-get install -y rpm python3-setuptools
diff --git a/osbuild/__init__.py b/osbuild/__init__.py
index 714779b5..099c5c9e 100644
--- a/osbuild/__init__.py
+++ b/osbuild/__init__.py
@@ -287,6 +287,13 @@ class Stage:
         self.name = name
         self.options = options
 
+    def description(self):
+        description = {}
+        description["name"] = self.name
+        if self.options:
+            description["options"] = self.options
+        return description
+
     def run(self, tree, build_tree, interactive=False, check=True, libdir=None):
         with BuildRoot(build_tree) as buildroot:
             if interactive:
@@ -322,6 +329,13 @@ class Assembler:
         self.name = name
         self.options = options
 
+    def description(self):
+        description = {}
+        description["name"] = self.name
+        if self.options:
+            description["options"] = self.options
+        return description
+
     def run(self, tree, build_tree, output_dir=None, interactive=False, check=True, libdir=None):
         with BuildRoot(build_tree) as buildroot:
             if interactive:
@@ -382,6 +396,18 @@ class Pipeline:
     def set_assembler(self, name, options=None):
         self.assembler = Assembler(name, options or {})
 
+    def description(self):
+        description = {}
+        if self.base:
+            description["base"] = self.base
+        if self.build:
+            description["build"] = self.build.description()
+        if self.stages:
+            description["stages"] = [s.description() for s in self.stages]
+        if self.assembler:
+            description["assembler"] = self.assembler.description()
+        return description
+
     @contextlib.contextmanager
     def get_buildtree(self, object_store):
         if self.build:
diff --git a/stages/org.osbuild.firewall b/stages/org.osbuild.firewall
new file mode 100755
index 00000000..8e76ee60
--- /dev/null
+++ b/stages/org.osbuild.firewall
@@ -0,0 +1,31 @@
+#!/usr/bin/python3
+
+import json
+import subprocess
+import sys
+
+
+def main(tree, options):
+    # Takes a list of <port|application protocol>:<transport protocol> pairs
+    ports = options["ports"]
+    # These must be defined for firewalld. It has a set of pre-defined services here: /usr/lib/firewalld/services/, but
+    # you can also define you own XML files in /etc/firewalld.
+    enabled_services = options["enabled_services"]
+    disabled_services = options["disabled_services"]
+
+    # firewall-offline-cmd does not implement --root option so we must chroot it
+    subprocess.run(["chroot",
+                    tree,
+                    "firewall-offline-cmd"] +
+                   list(map(lambda x: f"--port={x}", ports)) +
+                   list(map(lambda x: f"--service={x}", enabled_services)) +
+                   list(map(lambda x: f"--remove-service={x}", disabled_services)),
+                   check=True)
+
+    return 0
+
+
+if __name__ == '__main__':
+    args = json.load(sys.stdin)
+    r = main(args["tree"], args["options"])
+    sys.exit(r)


Test patch:
diff --git a/test/__init__.py b/test/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/test/firewall-test.json b/test/firewall-test.json
new file mode 100644
index 00000000..05cd3e4f
--- /dev/null
+++ b/test/firewall-test.json
@@ -0,0 +1,34 @@
+{
+  "name": "Example Image",
+  "stages": [
+    {
+      "name": "org.osbuild.dnf",
+      "options": {
+        "releasever": "30",
+        "repos": {
+          "fedora": {
+            "name": "Fedora",
+            "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch",
+            "gpgkey": "file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch"
+          }
+        },
+        "packages": ["@Core", "firewalld"]
+	}
+    },
+    {
+      "name": "org.osbuild.firewall",
+      "options": {
+        "ports": ["53:tcp", "88:udp"],
+        "enabled_services": ["http", "ftp"],
+        "disabled_services": ["telnet"]
+      }
+    }
+  ],
+  "assembler": {
+    "name": "org.osbuild.tar",
+    "options": {
+      "filename": "firewall-output.tar.xz",
+      "compression": "xz"
+    }
+  }
+}
diff --git a/test/run-tests.py b/test/run-tests.py
index 8507f724..ce982cba 100644
--- a/test/run-tests.py
+++ b/test/run-tests.py
@@ -70,14 +70,34 @@ def build_timezone_image():
     run_osbuild(rel_path("timezone-test.json"))
 
 
-def test_timezone():
+def build_firewall_image():
+    run_osbuild(rel_path("firewall-test.json"))
+
+
+def extract_to_tempdir(image_file):
     extract_dir = tempfile.mkdtemp(prefix="osbuild-")
-    subprocess.run(["tar", "xf", OUTPUT_DIR + "/timezone-output.tar.xz"], cwd=extract_dir, check=True)
+    subprocess.run(["tar", "xf", OUTPUT_DIR + image_file], cwd=extract_dir, check=True)
+    return extract_dir
+
+
+def test_timezone():
+    extract_dir = extract_to_tempdir("timezone-output.tar.xz")
     ls = subprocess.run(["ls", "-l", "etc/localtime"], cwd=extract_dir, check=True, stdout=subprocess.PIPE)
     ls_output = ls.stdout.decode("utf-8")
     assert "Europe/Prague" in ls_output
 
 
+def test_firewall():
+    extract_dir = extract_to_tempdir("firewall-output.tar.xz")
+    cat = subprocess.run(["cat", "etc/firewalld/zones/public.xml"], cwd=extract_dir, check=True, stdout=subprocess.PIPE)
+    cat_output = cat.stdout.decode("utf-8")
+    assert 'service name="http"' in cat_output
+    assert 'service name="ftp"' in cat_output
+    assert 'service name="telnet"' not in cat_output
+    assert 'port port="53" protocol="tcp"' in cat_output
+    assert 'port port="88" protocol="udp"' in cat_output
+
+
 def evaluate_test(test):
     try:
         test()
@@ -98,3 +118,5 @@ if __name__ == '__main__':
     build_timezone_image()
     evaluate_test(test_timezone)
 
+    build_firewall_image()
+    evaluate_test(test_firewall)
diff --git a/test/test_osbuild.py b/test/test_osbuild.py
new file mode 100644
index 00000000..1142bb28
--- /dev/null
+++ b/test/test_osbuild.py
@@ -0,0 +1,77 @@
+
+import osbuild
+import unittest
+
+
+class TestDescriptions(unittest.TestCase):
+    def test_canonical(self):
+        """Degenerate case. Make sure we always return the same canonical
+        description when passing empty or null values."""
+
+        cases = [
+            {},
+            { "assembler": None },
+            { "stages": [] },
+            { "build": {} },
+            { "build": None }
+        ]
+        for pipeline in cases:
+            with self.subTest(pipeline):
+                self.assertEqual(osbuild.load(pipeline).description(), {})
+
+    def test_stage(self):
+        name = "org.osbuild.test"
+        options = { "one": 1 }
+        cases = [
+            (osbuild.Stage(name, None, None, {}), {"name": name}),
+            (osbuild.Stage(name, None, None, None), {"name": name}),
+            (osbuild.Stage(name, None, None, options), {"name": name, "options": options}),
+        ]
+        for stage, description in cases:
+            with self.subTest(description):
+                self.assertEqual(stage.description(), description)
+
+    def test_assembler(self):
+        name = "org.osbuild.test"
+        options = { "one": 1 }
+        cases = [
+            (osbuild.Assembler(name, {}), {"name": name}),
+            (osbuild.Assembler(name, None), {"name": name}),
+            (osbuild.Assembler(name, options), {"name": name, "options": options}),
+        ]
+        for assembler, description in cases:
+            with self.subTest(description):
+                self.assertEqual(assembler.description(), description)
+
+    def test_pipeline(self):
+        build = osbuild.Pipeline()
+        build.add_stage("org.osbuild.test", { "one": 1 })
+
+        pipeline = osbuild.Pipeline()
+        pipeline.set_build(build)
+        pipeline.add_stage("org.osbuild.test", { "one": 2 })
+        pipeline.set_assembler("org.osbuild.test")
+
+        self.assertEqual(pipeline.description(), {
+              "build": {
+                "stages": [
+                  {
+                    "name": "org.osbuild.test",
+                    "options": { "one": 1 }
+                  }
+                ]
+              },
+              "stages": [
+                {
+                  "name": "org.osbuild.test",
+                  "options": { "one": 2 }
+                }
+              ],
+              "assembler": {
+                "name": "org.osbuild.test"
+              }
+            })
+
+
+if __name__ == "__main__":
+    unittest.main()