diff --git a/os_apply_config/apply_config.py b/os_apply_config/apply_config.py index 40c268a..4561c56 100755 --- a/os_apply_config/apply_config.py +++ b/os_apply_config/apply_config.py @@ -113,6 +113,10 @@ def write_file(path, obj): else: mode, uid, gid = 0o644, -1, -1 mode = obj.mode or mode + if obj.owner is not None: + uid = obj.owner + if obj.group is not None: + gid = obj.group d = os.path.dirname(path) os.path.exists(d) or os.makedirs(d) diff --git a/os_apply_config/oac_file.py b/os_apply_config/oac_file.py index 17c54f3..c09b6e0 100644 --- a/os_apply_config/oac_file.py +++ b/os_apply_config/oac_file.py @@ -13,6 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import grp +import pwd + import six from os_apply_config import config_exception as exc @@ -22,6 +25,8 @@ class OacFile(object): DEFAULTS = { 'allow_empty': True, 'mode': None, + 'owner': None, + 'group': None, } def __init__(self, body, **kwargs): @@ -79,7 +84,7 @@ class OacFile(object): @mode.setter def mode(self, v): - """Returns the file mode. + """Pass in the mode to set on the file. EG 0644. Must be between 0 and 0777, the sticky bit is not supported. """ @@ -88,3 +93,51 @@ class OacFile(object): if not 0 <= v <= 0o777: raise exc.ConfigException("mode '%#o' out of range" % v) self._mode = v + + @property + def owner(self): + """The UID to set on the file, EG 'rabbitmq' or '501'.""" + return self._owner + + @owner.setter + def owner(self, v): + """Pass in the UID to set on the file. + + EG 'rabbitmq' or 501. + """ + try: + if type(v) is int: + user = pwd.getpwuid(v) + elif type(v) is str: + user = pwd.getpwnam(v) + else: + raise exc.ConfigException( + "owner '%s' must be a string or int" % v) + except KeyError: + raise exc.ConfigException( + "owner '%s' not found in passwd database" % v) + self._owner = user[2] + + @property + def group(self): + """The GID to set on the file, EG 'rabbitmq' or '501'.""" + return self._group + + @group.setter + def group(self, v): + """Pass in the GID to set on the file. + + EG 'rabbitmq' or 501. + """ + try: + if type(v) is int: + group = grp.getgrgid(v) + elif type(v) is str: + group = grp.getgrnam(v) + else: + raise exc.ConfigException( + "group '%s' must be a string or int" % v) + except KeyError: + raise exc.ConfigException( + "group '%s' not found in group database" % v) + self._group = group[2] diff --git a/os_apply_config/tests/chown_templates/group.gid b/os_apply_config/tests/chown_templates/group.gid new file mode 100644 index 0000000..9630210 --- /dev/null +++ b/os_apply_config/tests/chown_templates/group.gid @@ -0,0 +1 @@ +lorem gido diff --git a/os_apply_config/tests/chown_templates/group.gid.oac b/os_apply_config/tests/chown_templates/group.gid.oac new file mode 100644 index 0000000..9dab1af --- /dev/null +++ b/os_apply_config/tests/chown_templates/group.gid.oac @@ -0,0 +1 @@ +group: 0 diff --git a/os_apply_config/tests/chown_templates/group.name b/os_apply_config/tests/chown_templates/group.name new file mode 100644 index 0000000..e7f4c24 --- /dev/null +++ b/os_apply_config/tests/chown_templates/group.name @@ -0,0 +1 @@ +namo gido diff --git a/os_apply_config/tests/chown_templates/group.name.oac b/os_apply_config/tests/chown_templates/group.name.oac new file mode 100644 index 0000000..40ee7a2 --- /dev/null +++ b/os_apply_config/tests/chown_templates/group.name.oac @@ -0,0 +1 @@ +group: root diff --git a/os_apply_config/tests/chown_templates/owner.name b/os_apply_config/tests/chown_templates/owner.name new file mode 100644 index 0000000..7490212 --- /dev/null +++ b/os_apply_config/tests/chown_templates/owner.name @@ -0,0 +1 @@ +namo uido diff --git a/os_apply_config/tests/chown_templates/owner.name.oac b/os_apply_config/tests/chown_templates/owner.name.oac new file mode 100644 index 0000000..cb39c9b --- /dev/null +++ b/os_apply_config/tests/chown_templates/owner.name.oac @@ -0,0 +1 @@ +owner: root diff --git a/os_apply_config/tests/chown_templates/owner.uid b/os_apply_config/tests/chown_templates/owner.uid new file mode 100644 index 0000000..29cef96 --- /dev/null +++ b/os_apply_config/tests/chown_templates/owner.uid @@ -0,0 +1 @@ +lorem uido diff --git a/os_apply_config/tests/chown_templates/owner.uid.oac b/os_apply_config/tests/chown_templates/owner.uid.oac new file mode 100644 index 0000000..fea9897 --- /dev/null +++ b/os_apply_config/tests/chown_templates/owner.uid.oac @@ -0,0 +1 @@ +owner: 0 diff --git a/os_apply_config/tests/test_apply_config.py b/os_apply_config/tests/test_apply_config.py index 58f6323..1e865e2 100644 --- a/os_apply_config/tests/test_apply_config.py +++ b/os_apply_config/tests/test_apply_config.py @@ -28,13 +28,6 @@ from os_apply_config import oac_file # example template tree TEMPLATES = os.path.join(os.path.dirname(__file__), 'templates') -TEMPLATE_PATHS = [ - "/etc/glance/script.conf", - "/etc/keystone/keystone.conf", - "/etc/control/empty", - "/etc/control/allow_empty", - "/etc/control/mode", -] # config for example tree CONFIG = { @@ -68,6 +61,17 @@ OUTPUT = { "/etc/control/mode": oac_file.OacFile( "lorem modus\n").set('mode', 0o755), } +TEMPLATE_PATHS = OUTPUT.keys() + +# expected output for chown tests +# separated out to avoid needing to mock os.chown for most tests +CHOWN_TEMPLATES = os.path.join(os.path.dirname(__file__), 'chown_templates') +CHOWN_OUTPUT = { + "owner.uid": oac_file.OacFile("lorem uido\n").set('owner', 0), + "owner.name": oac_file.OacFile("namo uido\n").set('owner', 0), + "group.gid": oac_file.OacFile("lorem gido\n").set('group', 0), + "group.name": oac_file.OacFile("namo gido\n").set('group', 0), +} def main_path(): @@ -340,3 +344,14 @@ class OSConfigApplierTestCase(testtools.TestCase): target_file = os.path.join(tmpdir, template[1:]) apply_config.install_config([path], TEMPLATES, tmpdir, False) self.assertEqual(0o100755, os.stat(target_file).st_mode) + + @mock.patch('os.chown') + def test_control_chown(self, chown_mock): + path = self.write_config(CONFIG) + tmpdir = tempfile.mkdtemp() + apply_config.install_config([path], CHOWN_TEMPLATES, tmpdir, False) + chown_mock.assert_has_calls([mock.call(mock.ANY, 0, -1), # uid + mock.call(mock.ANY, 0, -1), # username + mock.call(mock.ANY, -1, 0), # gid + mock.call(mock.ANY, -1, 0)], # groupname + any_order=True) diff --git a/os_apply_config/tests/test_oac_file.py b/os_apply_config/tests/test_oac_file.py index 26b686f..36795d5 100644 --- a/os_apply_config/tests/test_oac_file.py +++ b/os_apply_config/tests/test_oac_file.py @@ -13,6 +13,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import grp +import pwd + import testtools from os_apply_config import config_exception as exc @@ -39,3 +42,49 @@ class OacFileTestCase(testtools.TestCase): for mode in [0, 0o777]: oacf.mode = mode + + def test_owner_positive(self): + oacf = oac_file.OacFile('') + users = pwd.getpwall() + for name in [user[0] for user in users]: + oacf.owner = name + for uid in [user[2] for user in users]: + oacf.owner = uid + + def test_owner_negative(self): + oacf = oac_file.OacFile('') + try: + user = -1 + oacf.owner = user + except exc.ConfigException as e: + self.assertIn( + "owner '%s' not found in passwd database" % user, str(e)) + try: + user = "za" + oacf.owner = user + except exc.ConfigException as e: + self.assertIn( + "owner '%s' not found in passwd database" % user, str(e)) + + def test_group_positive(self): + oacf = oac_file.OacFile('') + groups = grp.getgrall() + for name in [group[0] for group in groups]: + oacf.group = name + for gid in [group[2] for group in groups]: + oacf.group = gid + + def test_group_negative(self): + oacf = oac_file.OacFile('') + try: + group = -1 + oacf.group = group + except exc.ConfigException as e: + self.assertIn( + "group '%s' not found in group database" % group, str(e)) + try: + group = "za" + oacf.group = group + except exc.ConfigException as e: + self.assertIn( + "group '%s' not found in group database" % group, str(e))