lorax/tests/composer/test_compose.py
Brian C. Lane 206d9a34a6 composer-cli: Add support for start-ostree --url URL
Some ostree builds, like edge, require passing a url to it along with
the ref and parent arguments. This adds an optional --url argument to
the 'composer-cli compose start-ostree' command.

Resolves: rhbz#1929381
2021-02-16 13:37:23 -08:00

519 lines
24 KiB
Python

#
# Copyright (C) 2020 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from http import HTTPStatus
from http.server import BaseHTTPRequestHandler
import json
import shutil
from socketserver import UnixStreamServer
import threading
import tempfile
import unittest
from composer import http_client as client
import composer.cli as cli
from composer.cli.cmdline import composer_cli_parser
from composer.cli.compose import get_size
# Use a GLOBAL record the request data for use in the test
# because there is no way to access the request handler class from the test methods
LAST_REQUEST = {}
# Test data for upload profile
PROFILE_TOML = """
provider = "aws"
[settings]
aws_access_key = "AWS Access Key"
aws_bucket = "AWS Bucket"
aws_region = "AWS Region"
aws_secret_key = "AWS Secret Key"
"""
class MyUnixServer(UnixStreamServer):
def get_request(self):
"""There is no client address for Unix Domain Sockets, so return the server address"""
req, _ = self.socket.accept()
return (req, self.server_address)
class APIHTTPHandler(BaseHTTPRequestHandler):
STATUS = {}
def log_request(self, code='-', size='-'):
# Don't log requests
return
def send_json_response(self, status, d):
"""Send a 200 with a JSON body"""
body = json.dumps(d).encode("UTF-8")
self.send_response(status)
self.send_header("Content-Type", "application/json")
self.send_header('Content-Length', str(len(body)))
self.end_headers()
self.wfile.write(body)
def send_api_status_dict(self):
self.send_json_response(HTTPStatus.OK, self.STATUS)
def send_api_status(self, status=True, errors=None):
if status:
self.send_json_response(HTTPStatus.OK, {"status": True})
else:
self.send_json_response(HTTPStatus.BAD_REQUEST,
{"status": False, "errors": [{"id": "0", "msg": "Test Framework"}]})
def save_request(self):
global LAST_REQUEST
LAST_REQUEST = {
"command": self.command,
"path": self.path,
"headers": self.headers,
"body": "",
}
try:
length = int(self.headers.get('content-length'))
LAST_REQUEST["body"] = self.rfile.read(length)
except (ValueError, TypeError):
pass
print("%s" % LAST_REQUEST)
def do_GET(self):
self.save_request()
if self.path == "/api/status":
self.send_api_status_dict()
def do_POST(self):
# Need to check for /api/status and send the correct response
self.save_request()
self.send_api_status(True)
class LoraxAPIv0HTTPHandler(APIHTTPHandler):
STATUS = {
"api": "0",
"backend": "lorax-composer",
"build": "devel",
"db_supported": True,
"db_version": "0",
"msgs": [],
"schema_version": "0"
}
class LoraxAPIv1HTTPHandler(APIHTTPHandler):
STATUS = {
"api": "1",
"backend": "lorax-composer",
"build": "devel",
"db_supported": True,
"db_version": "0",
"msgs": [],
"schema_version": "0"
}
class OsBuildAPIv0HTTPHandler(APIHTTPHandler):
STATUS = {
"api": "0",
"backend": "osbuild-composer",
"build": "devel",
"db_supported": True,
"db_version": "0",
"msgs": [],
"schema_version": "0"
}
class OsBuildAPIv1HTTPHandler(APIHTTPHandler):
STATUS = {
"api": "1",
"backend": "osbuild-composer",
"build": "devel",
"db_supported": True,
"db_version": "0",
"msgs": [],
"schema_version": "0"
}
class ComposeTestCase(unittest.TestCase):
def run_args(self, args):
global LAST_REQUEST
LAST_REQUEST = {}
p = composer_cli_parser()
opts = p.parse_args(args)
status = cli.main(opts)
LAST_REQUEST["cli_status"] = status
return LAST_REQUEST
class ComposeLoraxV0TestCase(ComposeTestCase):
@classmethod
def setUpClass(self):
self.tmpdir = tempfile.mkdtemp(prefix="composer-cli.test.")
self.socket = self.tmpdir + "/api.socket"
self.server = MyUnixServer(self.socket, LoraxAPIv0HTTPHandler)
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
@classmethod
def tearDownClass(self):
self.server.shutdown()
self.thread.join(10)
shutil.rmtree(self.tmpdir)
def test_status(self):
"""Make sure the mock status response is working"""
global LAST_REQUEST
LAST_REQUEST = {}
result = client.get_url_json(self.socket, "/api/status")
self.assertTrue("path" in LAST_REQUEST)
self.assertEqual(LAST_REQUEST["path"], "/api/status")
self.assertEqual(result, LoraxAPIv0HTTPHandler.STATUS)
def test_compose_start_plain(self):
result = self.run_args(["--socket", self.socket, "--api", "0", "compose", "start", "http-server", "qcow2"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master"})
class ComposeLoraxV1TestCase(ComposeTestCase):
@classmethod
def setUpClass(self):
self.tmpdir = tempfile.mkdtemp(prefix="composer-cli.test.")
self.socket = self.tmpdir + "api.socket"
self.server = MyUnixServer(self.socket, LoraxAPIv1HTTPHandler)
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
@classmethod
def tearDownClass(self):
self.server.shutdown()
self.thread.join(10)
shutil.rmtree(self.tmpdir)
def test_status(self):
"""Make sure the mock status response is working"""
global LAST_REQUEST
LAST_REQUEST = {}
result = client.get_url_json(self.socket, "/api/status")
self.assertTrue("path" in LAST_REQUEST)
self.assertEqual(LAST_REQUEST["path"], "/api/status")
self.assertEqual(result, LoraxAPIv1HTTPHandler.STATUS)
def test_compose_start_plain(self):
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start", "http-server", "qcow2"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master"})
def test_compose_start_upload(self):
with tempfile.NamedTemporaryFile(prefix="composer-cli.test.") as f:
f.write(PROFILE_TOML.encode("UTF-8"))
f.seek(0)
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start", "http-server", "qcow2", "httpimage", f.name])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master",
"upload": {"image_name": "httpimage", "provider": "aws",
"settings": {"aws_access_key": "AWS Access Key", "aws_bucket": "AWS Bucket", "aws_region": "AWS Region", "aws_secret_key": "AWS Secret Key"}}})
def test_compose_start_provider(self):
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start", "http-server", "qcow2", "httpimage", "aws", "production"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master",
"upload": {"image_name": "httpimage", "profile": "production", "provider": "aws"}})
class ComposeOsBuildV0TestCase(ComposeTestCase):
@classmethod
def setUpClass(self):
self.tmpdir = tempfile.mkdtemp(prefix="composer-cli.test.")
self.socket = self.tmpdir + "api.socket"
self.server = MyUnixServer(self.socket, OsBuildAPIv0HTTPHandler)
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
@classmethod
def tearDownClass(self):
self.server.shutdown()
self.thread.join(10)
shutil.rmtree(self.tmpdir)
def test_status(self):
"""Make sure the mock status response is working"""
global LAST_REQUEST
LAST_REQUEST = {}
result = client.get_url_json(self.socket, "/api/status")
self.assertTrue("path" in LAST_REQUEST)
self.assertEqual(LAST_REQUEST["path"], "/api/status")
self.assertEqual(result, OsBuildAPIv0HTTPHandler.STATUS)
def test_compose_start_plain(self):
result = self.run_args(["--socket", self.socket, "--api", "0", "compose", "start", "http-server", "qcow2"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master"})
class ComposeOsBuildV1TestCase(ComposeTestCase):
@classmethod
def setUpClass(self):
self.tmpdir = tempfile.mkdtemp(prefix="composer-cli.test.")
self.socket = self.tmpdir + "api.socket"
self.server = MyUnixServer(self.socket, OsBuildAPIv1HTTPHandler)
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.daemon = True
self.thread.start()
@classmethod
def tearDownClass(self):
self.server.shutdown()
self.thread.join(10)
shutil.rmtree(self.tmpdir)
def test_status(self):
"""Make sure the mock status response is working"""
global LAST_REQUEST
LAST_REQUEST = {}
result = client.get_url_json(self.socket, "/api/status")
self.assertTrue("path" in LAST_REQUEST)
self.assertEqual(LAST_REQUEST["path"], "/api/status")
self.assertEqual(result, OsBuildAPIv1HTTPHandler.STATUS)
def test_compose_start_plain(self):
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start", "http-server", "qcow2"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master"})
def test_compose_start_upload(self):
with tempfile.NamedTemporaryFile(prefix="composer-cli.test.") as f:
f.write(PROFILE_TOML.encode("UTF-8"))
f.seek(0)
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start", "http-server", "qcow2", "httpimage", f.name])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master",
"upload": {"image_name": "httpimage", "provider": "aws",
"settings": {"aws_access_key": "AWS Access Key", "aws_bucket": "AWS Bucket", "aws_region": "AWS Region", "aws_secret_key": "AWS Secret Key"}}})
def test_compose_start_plain_size(self):
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start", "--size", "1776", "http-server", "qcow2"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master", "size": 1862270976})
def test_compose_start_size_upload(self):
with tempfile.NamedTemporaryFile(prefix="composer-cli.test.") as f:
f.write(PROFILE_TOML.encode("UTF-8"))
f.seek(0)
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start", "--size", "1791", "http-server", "qcow2", "httpimage", f.name])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "qcow2", "branch": "master", "size": 1877999616,
"upload": {"image_name": "httpimage", "provider": "aws",
"settings": {"aws_access_key": "AWS Access Key", "aws_bucket": "AWS Bucket", "aws_region": "AWS Region", "aws_secret_key": "AWS Secret Key"}}})
def test_compose_start_ostree_noargs(self):
"""Test start-ostree with no parent and no ref"""
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "http-server", "fedora-iot-commit"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "", "parent": ""}})
def test_compose_start_ostree_parent(self):
"""Test start-ostree with --parent"""
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--parent", "parenturl", "http-server", "fedora-iot-commit"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "", "parent": "parenturl"}})
def test_compose_start_ostree_ref(self):
"""Test start-ostree with --ref"""
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--ref", "refid", "http-server", "fedora-iot-commit"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "refid", "parent": ""}})
def test_compose_start_ostree_refparent(self):
"""Test start-ostree with --ref and --parent"""
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--ref", "refid", "--parent", "parenturl", "http-server", "fedora-iot-commit"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "refid", "parent": "parenturl"}})
def test_compose_start_ostree_url(self):
"""Test start-ostree with --ref, --parent, and --url"""
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--ref", "refid", "--parent", "parenturl", "--url", "http://some/path/", "http-server", "fedora-iot-commit"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "refid", "parent": "parenturl", "url": "http://some/path/"}})
def test_compose_start_ostree_size(self):
"""Test start-ostree with --size, --ref and --parent"""
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--size", "2048", "--ref", "refid", "--parent", "parenturl", "http-server", "fedora-iot-commit"])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"size": 2147483648,
"ostree": {"ref": "refid", "parent": "parenturl"}})
def test_compose_start_ostree_missing(self):
"""Test start-ostree with missing argument"""
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "http-server"])
self.assertTrue(result is not None)
self.assertTrue("cli_status" in result)
self.assertEqual(result["cli_status"], 1)
def test_compose_start_ostree_upload_parent(self):
"""Test start-ostree upload with --parent"""
with tempfile.NamedTemporaryFile(prefix="composer-cli.test.") as f:
f.write(PROFILE_TOML.encode("UTF-8"))
f.seek(0)
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--parent", "parenturl", "http-server", "fedora-iot-commit", "httpimage", f.name])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "", "parent": "parenturl"},
"upload": {"image_name": "httpimage", "provider": "aws",
"settings": {"aws_access_key": "AWS Access Key", "aws_bucket": "AWS Bucket", "aws_region": "AWS Region", "aws_secret_key": "AWS Secret Key"}}})
def test_compose_start_ostree_upload_ref(self):
"""Test start-ostree upload with --ref"""
with tempfile.NamedTemporaryFile(prefix="composer-cli.test.") as f:
f.write(PROFILE_TOML.encode("UTF-8"))
f.seek(0)
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--ref", "refid", "http-server", "fedora-iot-commit", "httpimage", f.name])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "refid", "parent": ""},
"upload": {"image_name": "httpimage", "provider": "aws",
"settings": {"aws_access_key": "AWS Access Key", "aws_bucket": "AWS Bucket", "aws_region": "AWS Region", "aws_secret_key": "AWS Secret Key"}}})
def test_compose_start_ostree_upload_refparent(self):
"""Test start-ostree upload with --ref and --parent"""
with tempfile.NamedTemporaryFile(prefix="composer-cli.test.") as f:
f.write(PROFILE_TOML.encode("UTF-8"))
f.seek(0)
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--parent", "parenturl", "--ref", "refid", "http-server", "fedora-iot-commit", "httpimage", f.name])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "refid", "parent": "parenturl"},
"upload": {"image_name": "httpimage", "provider": "aws",
"settings": {"aws_access_key": "AWS Access Key", "aws_bucket": "AWS Bucket", "aws_region": "AWS Region", "aws_secret_key": "AWS Secret Key"}}})
def test_compose_start_ostree_upload_size(self):
"""Test start-ostree upload with --size, --ref and --parent"""
with tempfile.NamedTemporaryFile(prefix="composer-cli.test.") as f:
f.write(PROFILE_TOML.encode("UTF-8"))
f.seek(0)
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "--size", "2048", "--parent", "parenturl", "--ref", "refid", "http-server", "fedora-iot-commit", "httpimage", f.name])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"size": 2147483648,
"ostree": {"ref": "refid", "parent": "parenturl"},
"upload": {"image_name": "httpimage", "provider": "aws",
"settings": {"aws_access_key": "AWS Access Key", "aws_bucket": "AWS Bucket", "aws_region": "AWS Region", "aws_secret_key": "AWS Secret Key"}}})
def test_compose_start_ostree_upload(self):
with tempfile.NamedTemporaryFile(prefix="composer-cli.test.") as f:
f.write(PROFILE_TOML.encode("UTF-8"))
f.seek(0)
result = self.run_args(["--socket", self.socket, "--api", "1", "compose", "start-ostree", "http-server", "fedora-iot-commit", "httpimage", f.name])
self.assertTrue(result is not None)
self.assertTrue("body" in result)
self.assertGreater(len(result["body"]), 0)
jd = json.loads(result["body"])
self.assertEqual(jd, {"blueprint_name": "http-server", "compose_type": "fedora-iot-commit", "branch": "master",
"ostree": {"ref": "", "parent": ""},
"upload": {"image_name": "httpimage", "provider": "aws",
"settings": {"aws_access_key": "AWS Access Key", "aws_bucket": "AWS Bucket", "aws_region": "AWS Region", "aws_secret_key": "AWS Secret Key"}}})
class SizeTest(unittest.TestCase):
def test_empty(self):
self.assertEqual(get_size([]), ([], 0))
def test_no_size(self):
self.assertEqual(get_size(["blueprint", "type", "imagename", "profile"]),
(["blueprint", "type", "imagename", "profile"], 0))
def test_size_later(self):
self.assertEqual(get_size(["start", "--size", "100", "type"]), (["start", "type"], 104857600))
def test_size_no_value(self):
with self.assertRaises(RuntimeError):
get_size(["--size"])
def test_size_nonint(self):
with self.assertRaises(ValueError):
get_size(["--size", "abc"])
def test_get_size(self):
self.assertEqual(get_size(["--size", "1912", "blueprint", "type"]),
(["blueprint", "type"], 2004877312))