add job kill to api

- kolla user must be killer, need to do this from a
sudoer-allowed script
- rename password-editor script to kolla_actions
- update rpm builder to handle rename
- add job kill function to kolla_actions
- add job kill to api
- add utest for kill

Jira-Issue: OSTACKDEV-32
This commit is contained in:
Steve Noyes 2016-04-07 09:41:44 -04:00
parent 7b14d4f142
commit 2a9497fd09
7 changed files with 180 additions and 34 deletions

View File

@ -50,8 +50,6 @@ Requires: PyYAML >= 3.10
Requires: /usr/bin/ssh-keygen
Conflicts: pexpect = 3.3
%description
The KollaCLI simplifies OpenStack Kolla deployments.
@ -105,7 +103,7 @@ rm -rf %{buildroot}
%attr(-, root, root) %{python_sitelib}
%attr(755, root, %{kolla_group}) %{_bindir}/kollacli
%attr(550, %{kolla_user}, %{kolla_group}) %dir %{_datadir}/kolla/kollacli/tools
%attr(500, %{kolla_user}, %{kolla_group}) %{_datadir}/kolla/kollacli/tools/passwd*
%attr(500, %{kolla_user}, %{kolla_group}) %{_datadir}/kolla/kollacli/tools/kolla_actions*
%attr(550, %{kolla_user}, %{kolla_group}) %{_datadir}/kolla/kollacli/tools/log_*
%attr(550, %{kolla_user}, %{kolla_group}) %{_datadir}/kolla/kollacli/ansible/*.yml
%attr(-, %{kolla_user}, %{kolla_group}) %config(noreplace) %{_sysconfdir}/kolla/kollacli
@ -154,12 +152,16 @@ sed -i "s/#retry_files_enabled = False/retry_files_enabled = False/" /etc/ansibl
/usr/bin/kollacli complete >/etc/bash_completion.d/kollacli 2>/dev/null
# Update the sudoers file
if ! grep -q 'kollacli/tools/passwd_editor' /etc/sudoers.d/%{kolla_user}
if ! grep -q 'kollacli/tools/kolla_actions' /etc/sudoers.d/%{kolla_user}
then
sed -i \
'/^Cmnd_Alias.*KOLLA_CMDS/ s:$:, %{_datadir}/kolla/kollacli/tools/passwd_editor.py:'\
'/^Cmnd_Alias.*KOLLA_CMDS/ s:$:, %{_datadir}/kolla/kollacli/tools/kolla_actions.py:'\
/etc/sudoers.d/%{kolla_user}
fi
# remove obsolete password editor from sudoers file
sed -i \
'/^Cmnd_Alias.*KOLLA_CMDS/ s:, /usr/share/kolla/kollacli/tools/passwd_editor.py::'\
/etc/sudoers.d/%{kolla_user}
# remove obsolete json_generator script
if test -f %{_datadir}/kolla/kollacli/tools/json_generator.py
@ -167,6 +169,13 @@ then
rm -f %{_datadir}/kolla/kollacli/tools/json_generator.py
fi
# remove obsolete password editor script
if test -f %{_datadir}/kolla/kollacli/tools/passwd_editor.py.py
then
rm -f %{_datadir}/kolla/kollacli/tools/passwd_editor.py.py
fi
%postun
case "$*" in
0)
@ -179,6 +188,9 @@ esac
%changelog
* Tue Apr 07 2016 - Steve Noyes <steve.noyes@oracle.com>
- rename passwd_editor.py to kolla_actions.py
* Tue Apr 05 2016 - Steve Noyes <steve.noyes@oracle.com>
- remove obsolete pexpect requirement

View File

@ -29,7 +29,10 @@ class Job(object):
def get_status(self):
"""Get status of job
:return: None if job still running, 0 if job succeeded, 1 if job failed
:return: None: job is still running
0: job succeeded
1: job failed
2: job killed by user
:rtype: int or None
"""
return self._ansible_job.get_status()
@ -49,3 +52,7 @@ class Job(object):
:rtype: string
"""
return self._ansible_job.get_command_output()
def kill(self):
"""kill the job"""
self._ansible_job.kill()

View File

@ -15,6 +15,7 @@ import fcntl
import json
import logging
import os
import pwd
import subprocess # nosec
import tempfile
import time
@ -22,7 +23,11 @@ import time
import kollacli.i18n as u
from kollacli.common.inventory import remove_temp_inventory
from kollacli.common.utils import PidManager
from kollacli.common.utils import get_kolla_actions_path
from kollacli.common.utils import get_admin_uids
from kollacli.common.utils import get_admin_user
from kollacli.common.utils import run_cmd
from kollacli.common.utils import safe_decode
LOG = logging.getLogger(__name__)
@ -57,6 +62,7 @@ class AnsibleJob(object):
self._process_std_err = None
self._errors = []
self._cmd_output = ''
self._kill_uname = None
def run(self):
try:
@ -73,6 +79,7 @@ class AnsibleJob(object):
stderr=subprocess.PIPE)
# setup stdout to be read without blocking
LOG.debug('process pid: %s' % self._process.pid)
flags = fcntl.fcntl(self._process.stdout, fcntl.F_GETFL)
fcntl.fcntl(self._process.stdout, fcntl.F_SETFL,
(flags | os.O_NONBLOCK))
@ -99,6 +106,7 @@ class AnsibleJob(object):
- None: running
- 0: done, success
- 1: done, error
- 2: done, killed by user
"""
status = self._process.poll()
self._read_from_callback()
@ -106,9 +114,14 @@ class AnsibleJob(object):
self._cmd_output = ''.join([self._cmd_output, out])
if status is not None:
# job has completed
status = self._process.returncode
if status != 0:
status = 1
if self._kill_uname:
status = 2
msg = u._('Job killed by user (%s)' % self._kill_uname)
self._errors = [msg]
else:
status = self._process.returncode
if status != 0:
status = 1
if not self._process_std_err:
# read stderr from process
std_err = self._read_stream(self._process.stderr)
@ -134,6 +147,31 @@ class AnsibleJob(object):
"""
return self._cmd_output
def kill(self):
"""kill job in progress
The process pid is owned by root, so
that is not killable. Need to kill all its children.
"""
# the kill must be run as the kolla user so the
# kolla_actions program must be used.
actions_path = get_kolla_actions_path()
kolla_user = get_admin_user()
cmd_prefix = ('/usr/bin/sudo -u %s %s job -t -p '
% (kolla_user, actions_path))
# kill the children from largest to smallest pids.
child_pids = PidManager.get_child_pids(self._process.pid)
for child_pid in sorted(child_pids, reverse=True):
cmd = ''.join([cmd_prefix, child_pid])
err_msg, output = run_cmd(cmd, print_output=False)
if err_msg:
LOG.debug('kill failed: %s %s' % (err_msg, output))
# record the name of user who killed the job
cur_uid = os.getuid()
self._kill_uname = pwd.getpwuid(cur_uid)[0]
def _read_stream(self, stream):
out = ''
if stream and not stream.closed:

View File

@ -19,7 +19,6 @@ from kollacli.api.exceptions import FailedOperation
from kollacli.common import utils
PWDS_FILENAME = 'passwords.yml'
PWD_EDITOR_FILENAME = 'passwd_editor.py'
def set_password(pwd_key, pwd_value):
@ -61,12 +60,10 @@ def get_password_names():
def _get_cmd_prefix():
editor_path = os.path.join(utils.get_kollacli_home(),
'tools',
PWD_EDITOR_FILENAME)
actions_path = utils.get_kolla_actions_path()
pwd_file_path = os.path.join(utils.get_kolla_etc(),
PWDS_FILENAME)
user = utils.get_admin_user()
prefix = '/usr/bin/sudo -u %s %s -p %s ' % (user,
editor_path, pwd_file_path)
prefix = ('/usr/bin/sudo -u %s %s password -p %s '
% (user, actions_path, pwd_file_path))
return prefix

View File

@ -56,6 +56,10 @@ def get_kolla_log_dir():
return '/var/log/kolla/'
def get_kolla_actions_path():
return os.path.join(get_kollacli_home(), 'tools', 'kolla_actions.py')
def get_admin_uids():
"""get uid and gid of admin user"""
user_info = pwd.getpwnam(get_admin_user())
@ -403,3 +407,30 @@ class Lock(object):
return False
else:
return False
class PidManager():
@staticmethod
def get_child_pids(pid, child_pids=[]):
"""get child pids of parent pid"""
# This ps command will return child pids of parent pid, separated by
# newlines.
err_msg, output = run_cmd('ps --ppid %s -o pid=""' % pid,
print_output=False)
# err_msg is expected when pid has no children
if not err_msg:
output = output.strip()
if '\n' in output:
ps_pids = output.split('\n')
else:
ps_pids = [output]
if ps_pids:
child_pids.extend(ps_pids)
# recurse through children to get all child pids
for ps_pid in ps_pids:
PidManager.get_child_pids(ps_pid, child_pids)
return child_pids

View File

@ -20,6 +20,8 @@ from common import TestConfig
from kollacli.api.client import ClientApi
import random
import time
import unittest
TEST_GROUP_NAME = 'test_group'
@ -93,8 +95,16 @@ class TestFunctional(KollaCliTest):
for predeploy_cmd in predeploy_cmds:
self.run_cli_cmd('%s' % predeploy_cmd)
# deploy limited services openstack
self.log.info('Start deploy')
# test killing a deploy
self.log.info('Kill a deployment')
job = CLIENT.async_deploy(verbose_level=2)
time.sleep(random.randint(1, 5))
job.kill()
self._process_job(job, 'deploy-kill',
is_physical_host, expect_kill=True)
# do a deploy of a limited set of services
self.log.info('Start a deployment')
job = CLIENT.async_deploy(verbose_level=2)
self._process_job(job, 'deploy', is_physical_host)
@ -159,19 +169,24 @@ class TestFunctional(KollaCliTest):
'is running on host: %s ' % hostname +
'after destroy.')
def _process_job(self, job, descr, is_physical_host):
def _process_job(self, job, descr, is_physical_host, expect_kill=False):
status = job.wait()
err_msg = job.get_error_message()
self.log.info('job is complete. status: %s, err: %s'
% (status, err_msg))
if is_physical_host:
self.assertEqual(0, status, 'Job %s failed: %s' % (descr, err_msg))
if expect_kill:
self.assertEqual(2, status, 'Job %s does not have killed status %s'
% (descr, err_msg))
else:
self.assertEqual(1, status, 'Job %s ' % descr +
'succeeded when it should have failed')
self.assertIn(UNREACHABLE,
'Job %s: No hosts, but got wrong error: %s'
% (descr, err_msg))
if is_physical_host:
self.assertEqual(0, status, 'Job %s failed: %s'
% (descr, err_msg))
else:
self.assertEqual(1, status, 'Job %s ' % descr +
'succeeded when it should have failed')
self.assertIn(UNREACHABLE,
'Job %s: No hosts, but got wrong error: %s'
% (descr, err_msg))
if __name__ == '__main__':
unittest.main()

View File

@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import getopt
import os
import signal
import sys
from kollacli.common.utils import change_property
@ -35,17 +37,17 @@ def _print_pwd_keys(path):
print(pwd_keys)
def main():
"""edit password in passwords.yml file
def _password_cmd(argv):
"""password command
sys.argv:
-p path # path to passwords.yaml
-k key # key of password
-v value # value of password
-c # flag to clear the password
-l # print to stdout a csv string of the existing keys
args for password command:
-p path # path to passwords.yaml
-k key # key of password
-v value # value of password
-c # flag to clear the password
-l # print to stdout a csv string of the existing keys
"""
opts, _ = getopt.getopt(sys.argv[1:], 'p:k:v:cl')
opts, _ = getopt.getopt(argv[2:], 'p:k:v:cl')
path = ''
pwd_key = ''
pwd_value = ''
@ -71,5 +73,49 @@ def main():
change_property(path, pwd_key, pwd_value, clear_flag)
def _job_cmd(argv):
"""jobs command
args for job command
-t # terminate action
-p pid # process pid
"""
opts, _ = getopt.getopt(argv[2:], 'tp:')
pid = None
term_flag = False
for opt, arg in opts:
if opt == '-p':
pid = arg
elif opt == '-t':
term_flag = True
if term_flag:
try:
os.kill(int(pid), signal.SIGKILL)
except Exception as e:
raise Exception('%s, pid %s' % (str(e), pid))
def main():
"""perform actions on behalf of kolla user
sys.argv:
sys.argv[1] # command
Supported commands:
- password
- job
"""
if len(sys.argv) <= 1:
raise Exception('Invalid number of parameters')
command = sys.argv[1]
if command == 'password':
_password_cmd(sys.argv)
elif command == 'job':
_job_cmd(sys.argv)
else:
raise Exception('Invalid command %s' % command)
if __name__ == '__main__':
main()