diff --git a/CREDITS.rst b/CREDITS.rst index fe6f58fe..cbef22bb 100644 --- a/CREDITS.rst +++ b/CREDITS.rst @@ -21,6 +21,7 @@ Contributors - Zahari Zahariev - Eldar Nugaev - Saad Zaher Saad +- Samuel Bartel Credits ======= diff --git a/freezer/arguments.py b/freezer/arguments.py index a72d35a8..7bc01f0c 100644 --- a/freezer/arguments.py +++ b/freezer/arguments.py @@ -64,7 +64,7 @@ DEFAULT_PARAMS = { 'restore_abs_path': False, 'log_file': None, 'upload': True, 'mode': 'fs', 'action': 'backup', 'vssadmin': True, 'shadow': '', 'shadow_path': '', - 'windows_volume': '' + 'windows_volume': '', 'command': None } @@ -128,11 +128,13 @@ def backup_arguments(args_dict={}): parents=[conf_parser]) arg_parser.add_argument( - '--action', choices=['backup', 'restore', 'info', 'admin'], + '--action', choices=['backup', 'restore', 'info', 'admin', + 'exec'], help=( "Set the action to be taken. backup and restore are" " self explanatory, info is used to retrieve info from the" - " storage media, while admin is used to delete old backups" + " storage media, exec is used to execute a script," + " while admin is used to delete old backups" " and other admin actions. Default backup."), dest='action', default='backup') arg_parser.add_argument( @@ -396,6 +398,10 @@ def backup_arguments(args_dict={}): help='''Create a backup using a snapshot on windows using vssadmin. Options are: True and False, default is True''', dest='vssadmin', default=True) + arg_parser.add_argument( + '--command', action='store', + help='Command executed by exec action', + dest='command', default=None) arg_parser.set_defaults(**defaults) backup_args = arg_parser.parse_args() diff --git a/freezer/exec_cmd.py b/freezer/exec_cmd.py new file mode 100644 index 00000000..37eff0a6 --- /dev/null +++ b/freezer/exec_cmd.py @@ -0,0 +1,60 @@ +""" +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +This product includes cryptographic software written by Eric Young +(eay@cryptsoft.com). This product includes software written by Tim +Hudson (tjh@cryptsoft.com). +======================================================================== + +Freezer script execution related functions +""" + +import subprocess + + +def execute(cmd): + """ + Split a command specified as function arguments into separate sub commands + executed separately + """ + cmds = cmd.split('|') + nb_process = len(cmds) + index = 1 + process = None + for sub_cmd in cmds: + is_last_process = (index == nb_process) + process = popen_call(sub_cmd.split(' '), process, is_last_process) + index += 1 + + +def popen_call(sub_cmd, input, is_last_process): + """ + Execute a command specified as function arguments using the given input + """ + if not input: + process = subprocess.Popen(sub_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, shell=False) + else: + process = subprocess.Popen(sub_cmd, + stdout=subprocess.PIPE, stdin=input.stdout, + stderr=subprocess.PIPE, shell=False) + input.stdout.close() + if (is_last_process): + process.communicate()[0] + rc = process.returncode + if rc != 0: + raise Exception('Error: while executing script ' + '%s return code was %d instead of 0' + % (' '.join(sub_cmd), rc)) + return process diff --git a/freezer/job.py b/freezer/job.py index 1ea31316..cfb42763 100644 --- a/freezer/job.py +++ b/freezer/job.py @@ -23,7 +23,7 @@ from freezer import swift from freezer import utils from freezer import backup from freezer import restore - +from freezer import exec_cmd import logging from freezer.restore import RestoreOs @@ -149,6 +149,20 @@ class AdminJob(Job): swift.remove_obj_older_than(self.conf) +class ExecJob(Job): + @Job.executemethod + def execute(self): + logging.info('[*] exec job....') + if self.conf.command: + logging.info('[*] Executing exec job....') + exec_cmd.execute(self.conf.command) + else: + logging.warning( + '[*] No command info options were set. Exiting.') + return False + return True + + def create_job(conf): if conf.action == 'backup': return BackupJob(conf) @@ -158,4 +172,6 @@ def create_job(conf): return InfoJob(conf) if conf.action == 'admin': return AdminJob(conf) + if conf.action == 'exec': + return ExecJob(conf) raise Exception('Action "{0}" not supported'.format(conf.action)) diff --git a/freezer/main.py b/freezer/main.py index f70f7b0c..e2fef68f 100644 --- a/freezer/main.py +++ b/freezer/main.py @@ -94,7 +94,7 @@ def freezer_main(args={}): except Exception as priority_error: logging.warning('[*] Priority: {0}'.format(priority_error)) - # Alternative aruments provision useful to run Freezer without + # Alternative arguments provision useful to run Freezer without # command line e.g. functional testing if args: backup_args.__dict__.update(args) diff --git a/tests/commons.py b/tests/commons.py index bcdb24ee..6bddbe29 100644 --- a/tests/commons.py +++ b/tests/commons.py @@ -666,7 +666,7 @@ class FakeSwiftClient1: pass class Connection: - def __init__(self, key=True, os_options=True, os_auth_ver=True, user=True, authurl=True, tenant_name=True, retries=True, insecure=True): + def __init__(self, key=True, os_options=True, auth_version=True, user=True, authurl=True, tenant_name=True, retries=True, insecure=True): pass def put_object(self, opt1=True, opt2=True, opt3=True, opt4=True, opt5=True, headers=True, content_length=True, content_type=True): @@ -813,6 +813,7 @@ class BackupOpt1: nova_client = MagicMock() self.client_manager.get_nova = Mock(return_value=nova_client) + self.command = None class FakeMySQLdb: diff --git a/tests/test_exec_cmd.py b/tests/test_exec_cmd.py new file mode 100644 index 00000000..7245de4e --- /dev/null +++ b/tests/test_exec_cmd.py @@ -0,0 +1,61 @@ +"""Freezer pre_post_exec.py related tests + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +This product includes cryptographic software written by Eric Young +(eay@cryptsoft.com). This product includes software written by Tim +Hudson (tjh@cryptsoft.com). +======================================================================== + +""" + + + + +from freezer import exec_cmd +from mock import patch, Mock +import subprocess + + +from __builtin__ import True + + + +def test_exec_cmd(monkeypatch): + cmd="echo test > test.txt" + popen=patch('freezer.exec_cmd.subprocess.Popen') + mock_popen=popen.start() + mock_popen.return_value = Mock() + mock_popen.return_value.communicate = Mock() + mock_popen.return_value.communicate.return_value = ['some stderr'] + mock_popen.return_value.returncode = 0 + exec_cmd.execute(cmd) + assert (mock_popen.call_count == 1) + mock_popen.assert_called_with(['echo', 'test', '>', 'test.txt'], + shell=False, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + popen.stop() + + +def test__exec_cmd_with_pipe(monkeypatch): + cmd="echo test|wc -l" + popen=patch('freezer.exec_cmd.subprocess.Popen') + mock_popen=popen.start() + mock_popen.return_value = Mock() + mock_popen.return_value.communicate = Mock() + mock_popen.return_value.communicate.return_value = ['some stderr'] + mock_popen.return_value.returncode = 0 + exec_cmd.execute(cmd) + assert (mock_popen.call_count == 2) + popen.stop() diff --git a/tests/test_job.py b/tests/test_job.py index 25fbd397..a3a23f39 100644 --- a/tests/test_job.py +++ b/tests/test_job.py @@ -22,16 +22,19 @@ Hudson (tjh@cryptsoft.com). """ from commons import * -from freezer import (swift, restore, backup) - -from freezer.job import Job, InfoJob, AdminJob, BackupJob, RestoreJob, create_job +from freezer import ( + swift, restore, backup, exec_cmd) +from freezer.job import ( + Job, InfoJob, AdminJob, BackupJob, RestoreJob, ExecJob, create_job) import logging - +from mock import patch, Mock import pytest +import unittest class TestJob: + def do_monkeypatch(self, monkeypatch): fakelogging = FakeLogging() self.fakeswift = fakeswift = FakeSwift() @@ -143,6 +146,51 @@ class TestAdminJob(TestJob): assert job.execute() is None +class TestExecJob(TestJob): + + def setUp(self): + #init mock_popen + self.popen=patch('freezer.exec_cmd.subprocess.Popen') + self.mock_popen=self.popen.start() + self.mock_popen.return_value = Mock() + self.mock_popen.return_value.communicate = Mock() + self.mock_popen.return_value.communicate.return_value = ['some stderr'] + + + def tearDown(self): + self.popen.stop() + + + def test_execute_nothing_to_do(self, monkeypatch): + self.do_monkeypatch(monkeypatch) + backup_opt = BackupOpt1() + job = ExecJob(backup_opt) + assert job.execute() is False + + + def test_execute_script(self, monkeypatch): + self.setUp() + self.do_monkeypatch(monkeypatch) + self.mock_popen.return_value.returncode = 0 + backup_opt = BackupOpt1() + backup_opt.command='echo test' + job = ExecJob(backup_opt) + assert job.execute() is True + self.tearDown() + + + def test_execute_raise(self, monkeypatch): + self.setUp() + self.do_monkeypatch(monkeypatch) + popen=patch('freezer.exec_cmd.subprocess.Popen') + self.mock_popen.return_value.returncode = 1 + backup_opt = BackupOpt1() + backup_opt.command='echo test' + job = ExecJob(backup_opt) + pytest.raises(Exception, job.execute) + self.tearDown() + + def test_create_job(): backup_opt = BackupOpt1() backup_opt.action = None @@ -164,3 +212,6 @@ def test_create_job(): job = create_job(backup_opt) assert isinstance(job, AdminJob) + backup_opt.action = 'exec' + job = create_job(backup_opt) + assert isinstance(job, ExecJob) diff --git a/tests/test_main.py b/tests/test_main.py index 2c555bcf..5e4d9ff5 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -27,7 +27,6 @@ from commons import BackupOpt1 from freezer.main import freezer_main from freezer import job - import pytest import sys