def test_remove(run): """Test removing packages.""" remove(['package1', 'package2']) run.assert_has_calls( [call('packages', ['remove', '--packages', 'package1', 'package2'])]) run.reset_mock() run.side_effect = ActionError() remove(['package1']) run.assert_has_calls( [call('packages', ['remove', '--packages', 'package1'])])
def test_delete_repository_fail_view(rf): """Test that failed repository deletion sends correct error message.""" with patch('plinth.modules.gitweb.delete_repo', side_effect=ActionError('Error')): response, messages = make_request(rf.post(''), views.delete, name=EXISTING_REPOS[0]['name']) assert list( messages)[0].message == 'Could not delete {}: Error'.format( EXISTING_REPOS[0]['name']) assert response.status_code == 302
def test_disable_samba_share_failed_view(rf): """Test that share disabling failure sends correct error message.""" form_data = {'filesystem_type': 'ext4', 'open_share': 'disable'} mount_point = urllib.parse.quote('/') error_message = 'Unsharing failed' with patch('plinth.modules.samba.delete_share', side_effect=ActionError(error_message)): response, messages = make_request(rf.post('', data=form_data), views.share, mount_point=mount_point) assert list(messages)[ 0].message == 'Error disabling share: {0}'.format(error_message) assert response.status_code == 302 assert response.url == urls.reverse('samba:index')
def test_create_repo_failed_view(rf): """Test that repo creation failure sends correct error message.""" with patch('plinth.modules.gitweb.create_repo', side_effect=ActionError('Error')): form_data = { 'gitweb-name': 'something_other', 'gitweb-description': '', 'gitweb-owner': '' } request = rf.post(urls.reverse('gitweb:create'), data=form_data) view = views.CreateRepoView.as_view() response, messages = make_request(request, view) assert list(messages)[ 0].message == 'An error occurred while creating the repository.' assert response.status_code == 302
def test_edit_repository_failed_view(rf): """Test that failed repo editing sends correct error message.""" with patch('plinth.modules.gitweb.edit_repo', side_effect=ActionError('Error')): form_data = { 'gitweb-name': 'something_other', 'gitweb-description': 'test-description', 'gitweb-owner': 'test-owner' } request = rf.post( urls.reverse('gitweb:edit', kwargs={'name': EXISTING_REPOS[0]['name']}), data=form_data) view = views.EditRepoView.as_view() response, messages = make_request(request, view, **EXISTING_REPOS[0]) assert list( messages)[0].message == 'An error occurred during configuration.' assert response.status_code == 302
cmd += list(options) # No escaping necessary # Contract 1: commands can run via sudo. if run_as_root: cmd = ['sudo', '-n'] + cmd elif become_user: cmd = ['sudo', '-n', '-u', become_user] + cmd LOGGER.info('Executing command - %s', cmd) # Contract 3C: don't interpret shell escape sequences. # Contract 5 (and 6-ish). proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) if not async: output, error = proc.communicate(input=input) output, error = output.decode(), error.decode() if proc.returncode != 0: LOGGER.error('Error executing command - %s, %s, %s', cmd, output, error) raise ActionError(action, output, error) return output else: return proc
def _run(action, options=None, input=None, run_in_background=False, run_as_root=False, become_user=None, log_error=True): """Safely run a specific action as a normal user or root. Actions are pulled from the actions directory. - options are added to the action command. - input: data (as bytes) that will be sent to the action command's stdin. - run_in_background: run asynchronously or wait for the command to complete. - run_as_root: execute the command through sudo. """ if options is None: options = [] # Contract 3A and 3B: don't call anything outside of the actions directory. if os.sep in action: raise ValueError('Action cannot contain: ' + os.sep) cmd = os.path.join(cfg.actions_dir, action) if not os.path.realpath(cmd).startswith(cfg.actions_dir): raise ValueError('Action has to be in directory %s' % cfg.actions_dir) # Contract 3C: interpret shell escape sequences as literal file names. # Contract 3E: fail if the action doesn't exist or exists elsewhere. if not os.access(cmd, os.F_OK): raise ValueError('Action must exist in action directory.') cmd = [cmd] # Contract: 3C, 3D: don't allow shell special characters in # options be interpreted by the shell. When using # subprocess.Popen with list invocation and not shell invocation, # escaping is unnecessary as each argument is passed directly to # the command and not parsed by a shell. if options: if not isinstance(options, (list, tuple)): raise ValueError('Options must be list or tuple.') cmd += list(options) # No escaping necessary # Contract 1: commands can run via sudo. sudo_call = [] if run_as_root: sudo_call = ['sudo', '-n'] elif become_user: sudo_call = ['sudo', '-n', '-u', become_user] if cfg.develop and sudo_call: # Passing 'env' does not work with sudo, so append the PYTHONPATH # as part of the command sudo_call += ['PYTHONPATH=%s' % cfg.file_root] if sudo_call: cmd = sudo_call + cmd _log_command(cmd) # Contract 3C: don't interpret shell escape sequences. # Contract 5 (and 6-ish). kwargs = { 'stdin': subprocess.PIPE, 'stdout': subprocess.PIPE, 'stderr': subprocess.PIPE, 'shell': False, } if cfg.develop: # In development mode pass on local pythonpath to access Plinth kwargs['env'] = {'PYTHONPATH': cfg.file_root} proc = subprocess.Popen(cmd, **kwargs) if not run_in_background: output, error = proc.communicate(input=input) output, error = output.decode(), error.decode() if proc.returncode != 0: if log_error: logger.error('Error executing command - %s, %s, %s', cmd, output, error) raise ActionError(action, output, error) return output return proc
def _run(action, options=None, input=None, run_in_background=False, run_as_root=False, become_user=None, log_error=True): """Safely run a specific action as a normal user or root. Actions are pulled from the actions directory. - options are added to the action command. - input: data (as bytes) that will be sent to the action command's stdin. - run_in_background: run asynchronously or wait for the command to complete. - run_as_root: execute the command through sudo. """ if options is None: options = [] # Contract 3A and 3B: don't call anything outside of the actions directory. if os.sep in action: raise ValueError('Action cannot contain: ' + os.sep) cmd = os.path.join(cfg.actions_dir, action) if not os.path.realpath(cmd).startswith(cfg.actions_dir): raise ValueError('Action has to be in directory %s' % cfg.actions_dir) # Contract 3C: interpret shell escape sequences as literal file names. # Contract 3E: fail if the action doesn't exist or exists elsewhere. if not os.access(cmd, os.F_OK): raise ValueError('Action must exist in action directory.') cmd = [cmd] # Contract: 3C, 3D: don't allow shell special characters in # options be interpreted by the shell. When using # subprocess.Popen with list invocation and not shell invocation, # escaping is unnecessary as each argument is passed directly to # the command and not parsed by a shell. if options: if not isinstance(options, (list, tuple)): raise ValueError('Options must be list or tuple.') cmd += list(options) # No escaping necessary # Contract 1: commands can run via sudo. if run_as_root: cmd = ['sudo', '-n'] + cmd elif become_user: cmd = ['sudo', '-n', '-u', become_user] + cmd LOGGER.info('Executing command - %s', cmd) # Contract 3C: don't interpret shell escape sequences. # Contract 5 (and 6-ish). proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False) if not run_in_background: output, error = proc.communicate(input=input) output, error = output.decode(), error.decode() if proc.returncode != 0: if log_error: LOGGER.error('Error executing command - %s, %s, %s', cmd, output, error) raise ActionError(action, output, error) return output else: return proc