Merge "Add 'cluster-run' command"
This commit is contained in:
commit
0f98bc2807
@ -11,6 +11,8 @@
|
||||
# under the License.
|
||||
|
||||
import copy
|
||||
import subprocess
|
||||
|
||||
import mock
|
||||
import six
|
||||
import testtools
|
||||
@ -748,6 +750,175 @@ class ShellTest(testtools.TestCase):
|
||||
msg = _('Failed to delete some of the specified clusters.')
|
||||
self.assertEqual(msg, six.text_type(ex))
|
||||
|
||||
@mock.patch('subprocess.Popen')
|
||||
def test__run_script(self, mock_proc):
|
||||
x_proc = mock.Mock(returncode=0)
|
||||
x_stdout = 'OUTPUT'
|
||||
x_stderr = 'ERROR'
|
||||
x_proc.communicate.return_value = (x_stdout, x_stderr)
|
||||
mock_proc.return_value = x_proc
|
||||
|
||||
addr = {
|
||||
'private': [
|
||||
{
|
||||
'OS-EXT-IPS:type': 'floating',
|
||||
'version': 4,
|
||||
'addr': '1.2.3.4',
|
||||
}
|
||||
]
|
||||
}
|
||||
output = {}
|
||||
|
||||
sh._run_script('NODE_ID', addr, 'private', 'floating', 22, 'john',
|
||||
False, 'identity_path', 'echo foo', '-f bar',
|
||||
output=output)
|
||||
mock_proc.assert_called_once_with(
|
||||
['ssh', '-4', '-p22', '-i identity_path', '-f bar', 'john@1.2.3.4',
|
||||
'echo foo'],
|
||||
stdout=subprocess.PIPE)
|
||||
self.assertEqual(
|
||||
{'status': 'SUCCEEDED (0)', 'output': 'OUTPUT', 'error': 'ERROR'},
|
||||
output)
|
||||
|
||||
def test__run_script_network_not_found(self):
|
||||
addr = {'foo': 'bar'}
|
||||
output = {}
|
||||
|
||||
sh._run_script('NODE_ID', addr, 'private', 'floating', 22, 'john',
|
||||
False, 'identity_path', 'echo foo', '-f bar',
|
||||
output=output)
|
||||
self.assertEqual(
|
||||
{'status': 'FAILED',
|
||||
'reason': "Node 'NODE_ID' is not attached to network 'private'."
|
||||
},
|
||||
output)
|
||||
|
||||
def test__run_script_more_than_one_network(self):
|
||||
addr = {'foo': 'bar', 'koo': 'tar'}
|
||||
output = {}
|
||||
|
||||
sh._run_script('NODE_ID', addr, '', 'floating', 22, 'john',
|
||||
False, 'identity_path', 'echo foo', '-f bar',
|
||||
output=output)
|
||||
self.assertEqual(
|
||||
{'status': 'FAILED',
|
||||
'reason': "Node 'NODE_ID' is attached to more than one "
|
||||
"network. Please pick the network to use."},
|
||||
output)
|
||||
|
||||
def test__run_script_no_network(self):
|
||||
addr = {}
|
||||
output = {}
|
||||
|
||||
sh._run_script('NODE_ID', addr, '', 'floating', 22, 'john',
|
||||
False, 'identity_path', 'echo foo', '-f bar',
|
||||
output=output)
|
||||
|
||||
self.assertEqual(
|
||||
{'status': 'FAILED',
|
||||
'reason': "Node 'NODE_ID' is not attached to any network."},
|
||||
output)
|
||||
|
||||
def test__run_script_no_matching_address(self):
|
||||
addr = {
|
||||
'private': [
|
||||
{
|
||||
'OS-EXT-IPS:type': 'fixed',
|
||||
'version': 4,
|
||||
'addr': '1.2.3.4',
|
||||
}
|
||||
]
|
||||
}
|
||||
output = {}
|
||||
|
||||
sh._run_script('NODE_ID', addr, 'private', 'floating', 22, 'john',
|
||||
False, 'identity_path', 'echo foo', '-f bar',
|
||||
output=output)
|
||||
self.assertEqual(
|
||||
{'status': 'FAILED',
|
||||
'reason': "No address that would match network 'private' and "
|
||||
"type 'floating' of IPv4 has been found for node "
|
||||
"'NODE_ID'."},
|
||||
output)
|
||||
|
||||
def test__run_script_more_than_one_address(self):
|
||||
addr = {
|
||||
'private': [
|
||||
{
|
||||
'OS-EXT-IPS:type': 'fixed',
|
||||
'version': 4,
|
||||
'addr': '1.2.3.4',
|
||||
},
|
||||
{
|
||||
'OS-EXT-IPS:type': 'fixed',
|
||||
'version': 4,
|
||||
'addr': '5.6.7.8',
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
output = {}
|
||||
|
||||
sh._run_script('NODE_ID', addr, 'private', 'fixed', 22, 'john',
|
||||
False, 'identity_path', 'echo foo', '-f bar',
|
||||
output=output)
|
||||
self.assertEqual(
|
||||
{'status': 'FAILED',
|
||||
'reason': "More than one IPv4 fixed address found."},
|
||||
output)
|
||||
|
||||
@mock.patch('threading.Thread')
|
||||
@mock.patch.object(sh, '_run_script')
|
||||
def test_do_cluster_run(self, mock_run, mock_thread):
|
||||
service = mock.Mock()
|
||||
args = {
|
||||
'script': 'script_name',
|
||||
'network': 'network_name',
|
||||
'address_type': 'fixed',
|
||||
'port': 22,
|
||||
'user': 'root',
|
||||
'ipv6': False,
|
||||
'identity_file': 'identity_filename',
|
||||
'ssh_options': '-f oo',
|
||||
}
|
||||
args = self._make_args(args)
|
||||
args.id = 'CID'
|
||||
addr1 = {'addresses': 'ADDR CONTENT 1'}
|
||||
addr2 = {'addresses': 'ADDR CONTENT 2'}
|
||||
attributes = [
|
||||
mock.Mock(node_id='NODE1', attr_value=addr1),
|
||||
mock.Mock(node_id='NODE2', attr_value=addr2)
|
||||
]
|
||||
service.collect_cluster_attrs.return_value = attributes
|
||||
|
||||
th1 = mock.Mock()
|
||||
th2 = mock.Mock()
|
||||
mock_thread.side_effect = [th1, th2]
|
||||
fake_script = 'blah blah'
|
||||
with mock.patch('senlinclient.v1.shell.open',
|
||||
mock.mock_open(read_data=fake_script)) as mock_open:
|
||||
sh.do_cluster_run(service, args)
|
||||
|
||||
service.collect_cluster_attrs.assert_called_once_with(
|
||||
args.id, 'details')
|
||||
mock_open.assert_called_once_with('script_name', 'r')
|
||||
mock_thread.assert_has_calls([
|
||||
mock.call(target=mock_run,
|
||||
args=('NODE1', 'ADDR CONTENT 1', 'network_name',
|
||||
'fixed', 22, 'root', False, 'identity_filename',
|
||||
'blah blah', '-f oo'),
|
||||
kwargs={'output': {}}),
|
||||
mock.call(target=mock_run,
|
||||
args=('NODE2', 'ADDR CONTENT 2', 'network_name',
|
||||
'fixed', 22, 'root', False, 'identity_filename',
|
||||
'blah blah', '-f oo'),
|
||||
kwargs={'output': {}})
|
||||
])
|
||||
th1.start.assert_called_once_with()
|
||||
th2.start.assert_called_once_with()
|
||||
th1.join.assert_called_once_with()
|
||||
th2.join.assert_called_once_with()
|
||||
|
||||
@mock.patch.object(sh, '_show_cluster')
|
||||
def test_do_cluster_update(self, mock_show):
|
||||
service = mock.Mock()
|
||||
|
@ -11,8 +11,13 @@
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
from openstack import exceptions as sdk_exc
|
||||
import six
|
||||
|
||||
from senlinclient.common import exc
|
||||
from senlinclient.common.i18n import _
|
||||
from senlinclient.common.i18n import _LW
|
||||
@ -22,11 +27,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def show_deprecated(deprecated, recommended):
|
||||
logger.warning(_LW('"%(old)s" is deprecated, '
|
||||
'please use "%(new)s" instead.'),
|
||||
{'old': deprecated,
|
||||
'new': recommended}
|
||||
)
|
||||
logger.warning(
|
||||
_LW('"%(old)s" is deprecated, please use "%(new)s" instead.'),
|
||||
{'old': deprecated, 'new': recommended})
|
||||
|
||||
|
||||
def do_build_info(service, args=None):
|
||||
@ -593,6 +596,158 @@ def do_cluster_delete(service, args):
|
||||
print('Request accepted')
|
||||
|
||||
|
||||
def _run_script(node_id, addr, net, addr_type, port, user, ipv6, identity_file,
|
||||
script, options, output=None):
|
||||
version = 6 if ipv6 else 4
|
||||
|
||||
# Select the network to use.
|
||||
if net:
|
||||
addresses = addr.get(net)
|
||||
if not addresses:
|
||||
output['status'] = _('FAILED')
|
||||
output['reason'] = _("Node '%(node)s' is not attached to network "
|
||||
"'%(net)s'.") % {'node': node_id, 'net': net}
|
||||
return
|
||||
else:
|
||||
# network not specified
|
||||
if len(addr) > 1:
|
||||
output['status'] = _('FAILED')
|
||||
output['reason'] = _("Node '%(node)s' is attached to more than "
|
||||
"one network. Please pick the network to "
|
||||
"use.") % {'node': node_id}
|
||||
return
|
||||
elif not addr:
|
||||
output['status'] = _('FAILED')
|
||||
output['reason'] = _("Node '%(node)s' is not attached to any "
|
||||
"network.") % {'node': node_id}
|
||||
return
|
||||
else:
|
||||
addresses = list(six.itervalues(addr))[0]
|
||||
|
||||
# Select the address in the selected network.
|
||||
# If the extension is not present, we assume the address to be floating.
|
||||
matching_addresses = []
|
||||
for a in addresses:
|
||||
a_type = a.get('OS-EXT-IPS:type', 'floating')
|
||||
a_version = a.get('version')
|
||||
if (a_version == version and a_type == addr_type):
|
||||
matching_addresses.append(a.get('addr'))
|
||||
|
||||
if not matching_addresses:
|
||||
output['status'] = _('FAILED')
|
||||
output['reason'] = _("No address that would match network '%(net)s' "
|
||||
"and type '%(type)s' of IPv%(ver)s has been "
|
||||
"found for node '%(node)s'."
|
||||
) % {'net': net, 'type': addr_type,
|
||||
'ver': version, 'node': node_id}
|
||||
return
|
||||
|
||||
if len(matching_addresses) > 1:
|
||||
output['status'] = _('FAILED')
|
||||
output['reason'] = _("More than one IPv%(ver)s %(type)s address "
|
||||
"found.") % {'ver': version, 'type': addr_type}
|
||||
return
|
||||
|
||||
ip_address = str(matching_addresses[0])
|
||||
identity = '-i %s' % identity_file if identity_file else ''
|
||||
|
||||
cmd = [
|
||||
'ssh',
|
||||
'-%d' % version,
|
||||
'-p%d' % port,
|
||||
identity,
|
||||
options,
|
||||
'%s@%s' % (user, ip_address),
|
||||
'%s' % script
|
||||
]
|
||||
logger.debug("%s" % cmd)
|
||||
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE)
|
||||
(stdout, stderr) = proc.communicate()
|
||||
while proc.returncode is None:
|
||||
time.sleep(1)
|
||||
if proc.returncode == 0:
|
||||
output['status'] = _('SUCCEEDED (0)')
|
||||
output['output'] = stdout
|
||||
if stderr:
|
||||
output['error'] = stderr
|
||||
else:
|
||||
output['status'] = _('FAILED (%d)') % proc.returncode
|
||||
output['output'] = stdout
|
||||
if stderr:
|
||||
output['error'] = stderr
|
||||
|
||||
|
||||
@utils.arg("-p", "--port", metavar="<PORT>", type=int, default=22,
|
||||
help=_("Optional flag to indicate the port to use (Default=22)."))
|
||||
@utils.arg("-t", "--address-type", type=str, default="floating",
|
||||
help=_("Optional flag to indicate which IP type to use. Possible "
|
||||
"values includes 'fixed' and 'floating' (the Default)."))
|
||||
@utils.arg("-n", "--network", metavar='<NETWORK>', default='',
|
||||
help=_('Network to use for the ssh.'))
|
||||
@utils.arg("-6", "--ipv6", action="store_true", default=False,
|
||||
help=_("Optional flag to indicate whether to use an IPv6 address "
|
||||
"attached to a server. (Defaults to IPv4 address)"))
|
||||
@utils.arg("-u", "--user", metavar="<USER>", default="root",
|
||||
help=_("Login to use."))
|
||||
@utils.arg("-i", "--identity-file",
|
||||
help=_("Private key file, same as the '-i' option to the ssh "
|
||||
"command."))
|
||||
@utils.arg("-O", "--ssh-options", default="",
|
||||
help=_("Extra options to pass to ssh. see: man ssh."))
|
||||
@utils.arg("-s", "--script", metavar="<FILE>", required=True,
|
||||
help=_("Script file to run."))
|
||||
@utils.arg("id", metavar="<CLUSTER>",
|
||||
help=_('Name or ID of the cluster.'))
|
||||
def do_cluster_run(service, args):
|
||||
"""Run shell scripts on all nodes of a cluster."""
|
||||
if '@' in args.id:
|
||||
user, cluster = args.id.split('@', 1)
|
||||
args.user = user
|
||||
args.cluster = cluster
|
||||
|
||||
try:
|
||||
attributes = service.collect_cluster_attrs(args.id, 'details')
|
||||
except sdk_exc.ResourceNotFound:
|
||||
raise exc.CommandError(_("Cluster not found: %s") % args.id)
|
||||
|
||||
script = None
|
||||
try:
|
||||
f = open(args.script, 'r')
|
||||
script = f.read()
|
||||
except Exception:
|
||||
raise exc.CommandError(_("Cound not open script file: %s") %
|
||||
args.script)
|
||||
|
||||
tasks = dict()
|
||||
for attr in attributes:
|
||||
node_id = attr.node_id
|
||||
addr = attr.attr_value['addresses']
|
||||
|
||||
output = dict()
|
||||
th = threading.Thread(
|
||||
target=_run_script,
|
||||
args=(node_id, addr, args.network, args.address_type, args.port,
|
||||
args.user, args.ipv6, args.identity_file,
|
||||
script, args.ssh_options),
|
||||
kwargs={'output': output})
|
||||
th.start()
|
||||
tasks[th] = (node_id, output)
|
||||
|
||||
for t in tasks:
|
||||
t.join()
|
||||
|
||||
for t in tasks:
|
||||
node_id, result = tasks[t]
|
||||
print("node: %s" % node_id)
|
||||
print("status: %s" % result.get('status'))
|
||||
if "reason" in result:
|
||||
print("reason: %s" % result.get('reason'))
|
||||
if "output" in result:
|
||||
print("output:\n%s" % result.get('output'))
|
||||
if "error" in result:
|
||||
print("error:\n%s" % result.get('error'))
|
||||
|
||||
|
||||
@utils.arg('-p', '--profile', metavar='<PROFILE>',
|
||||
help=_('ID of new profile to use.'))
|
||||
@utils.arg('-t', '--timeout', metavar='<TIMEOUT>',
|
||||
|
Loading…
x
Reference in New Issue
Block a user