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:
parent
7b14d4f142
commit
2a9497fd09
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
Loading…
Reference in New Issue