diff --git a/snap_openstack/base.py b/snap_openstack/base.py index 59b4b4b..8318df4 100644 --- a/snap_openstack/base.py +++ b/snap_openstack/base.py @@ -41,6 +41,10 @@ DEFAULT_UWSGI_ARGS = ["--master", DEFAULT_NGINX_ARGS = ["-g", "daemon on; master_process on;"] +DEFAULT_OWNER = "root:root" +DEFAULT_DIR_MODE = 0o750 +DEFAULT_FILE_MODE = 0o640 + class OpenStackSnap(object): '''Main executor class for snap-openstack''' @@ -61,22 +65,34 @@ class OpenStackSnap(object): utils = SnapUtils() LOG.debug(setup) - if 'dirs' in setup.keys(): - for directory in setup['dirs']: - dir_name = directory.format(**utils.snap_env) - utils.ensure_dir(dir_name) + if 'users' in setup.keys(): + for user, groups in setup['users'].items(): + home = os.path.join("{snap_common}".format(**utils.snap_env), + "lib", user) + utils.add_user(user, groups, home) - if 'templates' in setup.keys(): - for template in setup['templates']: - target = setup['templates'][template] - target_file = target.format(**utils.snap_env) - utils.ensure_dir(target_file, is_file=True) - if not os.path.isfile(target_file): - LOG.debug('Rendering {} to {}'.format(template, - target_file)) - with open(target_file, 'w') as tf: - os.fchmod(tf.fileno(), 0o640) - tf.write(renderer.render(template, utils.snap_env)) + default_owner = setup.get('default-owner', DEFAULT_OWNER) + default_user, default_group = default_owner.split(':') + default_dir_mode = setup.get('default-dir-mode', DEFAULT_DIR_MODE) + default_file_mode = setup.get('default-file-mode', DEFAULT_FILE_MODE) + + for directory in setup.get('dirs', []): + dir_name = directory.format(**utils.snap_env) + utils.ensure_dir(dir_name, perms=default_dir_mode) + utils.rchmod(dir_name, default_dir_mode, default_file_mode) + utils.rchown(dir_name, default_user, default_group) + + for template in setup.get('templates', []): + target = setup['templates'][template] + target_file = target.format(**utils.snap_env) + utils.ensure_dir(target_file, is_file=True) + if not os.path.isfile(target_file): + LOG.debug('Rendering {} to {}'.format(template, + target_file)) + with open(target_file, 'w') as tf: + tf.write(renderer.render(template, utils.snap_env)) + utils.chmod(target_file, default_file_mode) + utils.chown(target_file, default_user, default_group) if 'copyfiles' in setup.keys(): for source, target in setup['copyfiles'].items(): @@ -89,6 +105,29 @@ class OpenStackSnap(object): continue LOG.debug('Copying file {} to {}'.format(s_file, d_file)) shutil.copy2(s_file, d_file) + utils.chmod(d_file, default_file_mode) + utils.chown(d_file, default_user, default_group) + + for target in setup.get('rchown', []): + target_path = target.format(**utils.snap_env) + user, group = setup['rchown'][target].split(':') + utils.rchown(target_path, user, group) + + for target in setup.get('chmod', []): + target_path = target.format(**utils.snap_env) + if os.path.exists(target_path): + mode = setup['chmod'][target] + utils.chmod(target_path, mode) + else: + LOG.debug('Path not found: {}'.format(target_path)) + + for target in setup.get('chown', []): + target_path = target.format(**utils.snap_env) + if os.path.exists(target_path): + user, group = setup['chown'][target].split(':') + utils.chown(target_path, user, group) + else: + LOG.debug('Path not found: {}'.format(target_path)) def execute(self, argv): '''Execute snap command building out configuration and log options''' @@ -165,5 +204,9 @@ class OpenStackSnap(object): LOG.debug('Configuration file {} not found' ', skipping'.format(cfile)) + if 'run-as' in entry_point.keys(): + user, groups = list(entry_point['run-as'].items())[0] + utils.drop_privileges(user, groups) + LOG.debug('Executing command {}'.format(' '.join(cmd))) os.execvp(cmd[0], cmd) diff --git a/snap_openstack/tests/data/snap-openstack.yaml b/snap_openstack/tests/data/snap-openstack.yaml index b3d2a5d..6ba46a6 100644 --- a/snap_openstack/tests/data/snap-openstack.yaml +++ b/snap_openstack/tests/data/snap-openstack.yaml @@ -13,6 +13,8 @@ entry_points: - "/etc/nova/nova.conf" config-dirs: - "/etc/nova/conf.d" + run-as: + nova: [nova] nova-scheduler: type: simple binary: nova-scheduler @@ -21,6 +23,8 @@ entry_points: config-dirs: - "/etc/nova/conf.d" log-file: "/var/log/nova/scheduler.log" + run-as: + nova: [nova] keystone-uwsgi: type: uwsgi uwsgi-dir: "/etc/uwsgi" @@ -28,6 +32,10 @@ entry_points: keystone-nginx: type: nginx config-file: "/etc/nginx/keystone/nginx.conf" + run-as: + nova: [nova] nova-broken: type: unknown binary: nova-broken + run-as: + nova: [nova] diff --git a/snap_openstack/tests/test_snap_openstack.py b/snap_openstack/tests/test_snap_openstack.py index 3e9d190..73fd7df 100644 --- a/snap_openstack/tests/test_snap_openstack.py +++ b/snap_openstack/tests/test_snap_openstack.py @@ -49,6 +49,7 @@ class TestOpenStackSnapExecute(test_base.TestCase): def mock_snap_utils(self, mock_utils): snap_utils = mock_utils.return_value snap_utils.snap_env = MOCK_SNAP_ENV + snap_utils.drop_privileges.return_value = None @patch('snap_openstack.base.SnapUtils') @patch.object(base, 'os') diff --git a/snap_openstack/utils.py b/snap_openstack/utils.py index d2c86ec..8a0544e 100644 --- a/snap_openstack/utils.py +++ b/snap_openstack/utils.py @@ -14,8 +14,11 @@ # License for the specific language governing permissions and limitations # under the License. +import grp import logging import os +import pwd +import subprocess LOG = logging.getLogger(__name__) @@ -57,7 +60,7 @@ class SnapUtils(object): ''' return self._snap_env - def ensure_dir(self, path, is_file=False): + def ensure_dir(self, path, is_file=False, perms=0o750): '''Ensure a directory exists Ensure that the directory structure to support the provided file or @@ -71,4 +74,69 @@ class SnapUtils(object): dir_name = os.path.dirname(path) if not os.path.exists(dir_name): LOG.info('Creating directory {}'.format(dir_name)) - os.makedirs(dir_name, 0o750) + os.makedirs(dir_name, perms) + + def add_user(self, user, groups, home): + '''Add user to the system as a member of one ore more groups''' + for group in groups: + try: + grp.getgrnam(group) + except KeyError: + LOG.debug('Adding group {} to system'.format(group)) + cmd = ['addgroup', '--system', group] + subprocess.check_call(cmd) + + try: + pwd.getpwnam(user) + except KeyError: + self.ensure_dir(home) + LOG.debug('Adding user {} to system'.format(user)) + cmd = ['adduser', '--quiet', '--system', '--home', home, + '--no-create-home', '--shell', '/bin/false', user] + subprocess.check_call(cmd) + + for group in groups: + LOG.debug('Adding user {} to group {}'.format(user, group)) + cmd = ['adduser', user, group] + subprocess.check_call(cmd) + + def chown(self, path, user, group): + '''Change the owner of the specified file''' + LOG.debug('Changing owner of {} to {}:{}'.format(path, user, group)) + uid = pwd.getpwnam(user).pw_uid + gid = grp.getgrnam(group).gr_gid + os.chown(path, uid, gid) + + def chmod(self, path, mode): + '''Change the file mode bits of the specified file''' + LOG.debug('Changing file mode of {} to {}'.format(path, oct(mode))) + os.chmod(path, mode) + + def rchown(self, root_dir, user, group): + '''Recursively change owner starting at the specified directory''' + self.chown(root_dir, user, group) + for dirpath, dirnames, filenames in os.walk(root_dir): + for d in dirnames: + self.chown(os.path.join(dirpath, d), user, group) + for f in filenames: + self.chown(os.path.join(dirpath, f), user, group) + + def rchmod(self, root_dir, dir_mode, file_mode): + '''Recursively change mode bits starting at the specified directory''' + self.chmod(root_dir, dir_mode) + for dirpath, dirnames, filenames in os.walk(root_dir): + for d in dirnames: + self.chmod(os.path.join(dirpath, d), dir_mode) + for f in filenames: + self.chmod(os.path.join(dirpath, f), file_mode) + + def drop_privileges(self, user, groups): + '''Drop privileges to the specified user and group(s)''' + LOG.debug('Dropping privileges to {}:{}'.format(user, groups)) + uid = pwd.getpwnam(user).pw_uid + gid = grp.getgrnam(groups[0]).gr_gid + gids = [grp.getgrnam(g).gr_gid for g in groups] + os.setgroups([]) + os.setgroups(gids) + os.setgid(gid) + os.setuid(uid)