diff --git a/src/pylorax/api/compose.py b/src/pylorax/api/compose.py index 44b3abf3..687fcd98 100644 --- a/src/pylorax/api/compose.py +++ b/src/pylorax/api/compose.py @@ -92,6 +92,78 @@ def repo_to_ks(r, url="url"): return cmd +def write_ks_user(f, user): + """ Write kickstart user and sshkey entry + + :param f: kickstart file object + :type f: open file object + :param user: A blueprint user dictionary + :type user: dict + + If the entry contains a ssh key, use sshkey to write it + All of the user fields are optional, except name, write out a kickstart user entry + with whatever options are relevant. + """ + if "name" not in user: + raise RuntimeError("user entry requires a name") + + # ssh key uses the sshkey kickstart command + if "key" in user: + f.write('sshkey --user %s "%s"\n' % (user["name"], user["key"])) + + # Write out the user kickstart command, much of it is optional + f.write("user --name %s" % user["name"]) + if "home" in user: + f.write(" --homedir %s" % user["home"]) + + if "password" in user: + if any(user["password"].startswith(prefix) for prefix in ["$2b$", "$6$", "$5$"]): + log.debug("Detected pre-crypted password") + f.write(" --iscrypted") + else: + log.debug("Detected plaintext password") + f.write(" --plaintext") + + f.write(" --password \"%s\"" % user["password"]) + + if "shell" in user: + f.write(" --shell %s" % user["shell"]) + + if "uid" in user: + f.write(" --uid %d" % int(user["uid"])) + + if "gid" in user: + f.write(" --gid %d" % int(user["gid"])) + + if "description" in user: + f.write(" --gecos \"%s\"" % user["description"]) + + if "groups" in user: + f.write(" --groups %s" % ",".join(user["groups"])) + + f.write("\n") + + +def write_ks_group(f, group): + """ Write kickstart group entry + + :param f: kickstart file object + :type f: open file object + :param group: A blueprint group dictionary + :type user: dict + + gid is optional + """ + if "name" not in group: + raise RuntimeError("group entry requires a name") + + f.write("group --name %s" % group["name"]) + if "gid" in group: + f.write(" --gid %d" % int(group["gid"])) + + f.write("\n") + + def add_customizations(f, recipe): """ Add customizations to the kickstart file @@ -109,13 +181,23 @@ def add_customizations(f, recipe): if "hostname" in customizations: f.write("network --hostname=%s\n" % customizations["hostname"]) + # TODO - remove this, should use user section to define this if "sshkey" in customizations: # This is a list of entries for sshkey in customizations["sshkey"]: if "user" not in sshkey or "key" not in sshkey: log.error("%s is incorrect, skipping", sshkey) continue - f.write('sshkey --user %s "%s"' % (sshkey["user"], sshkey["key"])) + f.write('sshkey --user %s "%s"\n' % (sshkey["user"], sshkey["key"])) + + if "user" in customizations: + # only name is required, everything else is optional + for user in customizations["user"]: + write_ks_user(f, user) + + if "group" in customizations: + for group in customizations["group"]: + write_ks_group(f, group) def start_build(cfg, yumlock, gitlock, branch, recipe_name, compose_type, test_mode=0): @@ -205,14 +287,14 @@ def start_build(cfg, yumlock, gitlock, branch, recipe_name, compose_type, test_m # Save a copy of the original kickstart shutil.copy(ks_template_path, results_dir) + with yumlock.lock: + repos = yumlock.yb.repos.listEnabled() + if not repos: + raise RuntimeError("No enabled repos, canceling build.") + # Create the final kickstart with repos and package list ks_path = joinpaths(results_dir, "final-kickstart.ks") with open(ks_path, "w") as f: - with yumlock.lock: - repos = yumlock.yb.repos.listEnabled() - if not repos: - raise RuntimeError("No enabled repos, canceling build.") - ks_url = repo_to_ks(repos[0], "url") log.debug("url = %s", ks_url) f.write('url %s\n' % ks_url) diff --git a/tests/pylorax/blueprints/custom-base.toml b/tests/pylorax/blueprints/custom-base.toml index 7528b6b2..e9a6c627 100644 --- a/tests/pylorax/blueprints/custom-base.toml +++ b/tests/pylorax/blueprints/custom-base.toml @@ -12,3 +12,30 @@ hostname = "custombase" [[customizations.sshkey]] user = "root" key = "A SSH KEY FOR ROOT" + +[[customizations.user]] +name = "widget" +description = "Widget process user account" +home = "/srv/widget/" +shell = "/usr/bin/false" +groups = ["dialout", "users"] + +[[customizations.user]] +name = "admin" +description = "Widget admin account" +password = "$6$CHO2$3rN8eviE2t50lmVyBYihTgVRHcaecmeCk31LeOUleVK/R/aeWVHVZDi26zAH.o0ywBKH9Tc0/wm7sW/q39uyd1" +home = "/srv/widget/" +shell = "/usr/bin/bash" +groups = ["widget", "users"] +uid = 1200 + +[[customizations.user]] +name = "plain" +password = "simple plain password" + +[[customizations.user]] +name = "bart" +key = "SSH KEY FOR BART" + +[[customizations.group]] +name = "widget" diff --git a/tests/pylorax/test_compose.py b/tests/pylorax/test_compose.py new file mode 100644 index 00000000..e9e38647 --- /dev/null +++ b/tests/pylorax/test_compose.py @@ -0,0 +1,161 @@ +# +# Copyright (C) 2018 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 . +# +from StringIO import StringIO +import unittest + +from pylorax.api.compose import add_customizations +from pylorax.api.recipes import recipe_from_toml + +BASE_RECIPE = """name = "test-cases" +description = "Used for testing" +version = "0.0.1" + +""" + +HOSTNAME = BASE_RECIPE + """[customizations] +hostname = "testhostname" +""" + +SSHKEY = BASE_RECIPE + """[[customizations.sshkey]] +user = "root" +key = "ROOT SSH KEY" +""" + +USER = BASE_RECIPE + """[[customizations.user]] +name = "tester" +""" + +USER_KEY = """ +key = "A SSH KEY FOR TESTER" +""" + +USER_DESC = """ +description = "a test user account" +""" + +USER_CRYPT = """ +password = "$6$CHO2$3rN8eviE2t50lmVyBYihTgVRHcaecmeCk31LeOUleVK/R/aeWVHVZDi26zAH.o0ywBKH9Tc0/wm7sW/q39uyd1" +""" + +USER_PLAIN = """ +password = "plainpassword" +""" + +USER_HOME = """ +home = "/opt/users/tester/" +""" + +USER_SHELL = """ +shell = "/usr/bin/zsh" +""" + +USER_UID = """ +uid = 1013 +""" + +USER_GID = """ +gid = 4242 +""" + +USER_GROUPS = """ +groups = ["wheel", "users"] +""" + +USER_ALL = USER + USER_KEY + USER_DESC + USER_CRYPT + USER_HOME + USER_SHELL + USER_UID + USER_GID + +GROUP = BASE_RECIPE + """[[customizations.group]] +name = "testgroup" +""" + +GROUP_GID = GROUP + """ +gid = 1011 +""" + +KS_USER_ALL = '''sshkey --user tester "A SSH KEY FOR TESTER" +user --name tester --homedir /opt/users/tester/ --iscrypted --password "$6$CHO2$3rN8eviE2t50lmVyBYihTgVRHcaecmeCk31LeOUleVK/R/aeWVHVZDi26zAH.o0ywBKH9Tc0/wm7sW/q39uyd1" --shell /usr/bin/zsh --uid 1013 --gid 4242 --gecos "a test user account" +''' + +class CustomizationsTestCase(unittest.TestCase): + def assertCustomization(self, test, result): + r = recipe_from_toml(test) + f = StringIO() + add_customizations(f, r) + self.assertTrue(result in f.getvalue(), f.getvalue()) + + def test_set_hostname(self): + """Test setting the hostname""" + self.assertCustomization(HOSTNAME, "network --hostname=testhostname") + + def test_set_sshkey(self): + """Test setting sshkey without user""" + self.assertCustomization(SSHKEY, 'sshkey --user root "ROOT SSH KEY"') + + def test_sshkey_only(self): + """Test adding a sshkey to an existing user account""" + self.assertCustomization(USER + USER_KEY, 'sshkey --user tester "A SSH KEY FOR TESTER"') + + def test_create_user(self): + """Test creating a user with no options""" + self.assertCustomization(USER, "user --name tester") + + def test_create_user_desc(self): + """Test creating a user with a description""" + self.assertCustomization(USER + USER_DESC, '--gecos "a test user account"') + + def test_create_user_crypt(self): + """Test creating a user with a pre-crypted password""" + self.assertCustomization(USER + USER_CRYPT, '--password "$6$CHO2$3r') + + def test_create_user_plain(self): + """Test creating a user with a plaintext password""" + self.assertCustomization(USER + USER_PLAIN, '--password "plainpassword"') + + def test_create_user_home(self): + """Test creating user with a home directory""" + self.assertCustomization(USER + USER_HOME, "--homedir /opt/users/tester/") + + def test_create_user_shell(self): + """Test creating user with shell set""" + self.assertCustomization(USER + USER_SHELL, "--shell /usr/bin/zsh") + + def test_create_user_uid(self): + """Test creating user with uid set""" + self.assertCustomization(USER + USER_UID, "--uid 1013") + + def test_create_user_gid(self): + """Test creating user with gid set""" + self.assertCustomization(USER + USER_GID, "--gid 4242") + + def test_create_user_groups(self): + """Test creating user with group membership""" + self.assertCustomization(USER + USER_GROUPS, "--groups wheel,users") + + def test_create_user_all(self): + """Test creating user with all settings""" + r = recipe_from_toml(USER_ALL) + f = StringIO() + add_customizations(f, r) + self.assertEqual(KS_USER_ALL, f.getvalue()) + + def test_create_group(self): + """Test creating group without gid set""" + self.assertCustomization(GROUP, "group --name testgroup") + + def test_create_group_gid(self): + """Test creating group with gid set""" + self.assertCustomization(GROUP_GID, "group --name testgroup --gid 1011") +