PR#2933: Enable/disable channel

Merges #2933
https://pagure.io/koji/pull-request/2933

Fixes: #1851
https://pagure.io/koji/issue/1851
[RFE] disable channel
This commit is contained in:
Tomas Kopecek 2021-07-20 10:33:59 +02:00
commit cfcf900e25
23 changed files with 659 additions and 80 deletions

View file

@ -45,7 +45,8 @@ from koji_cli.lib import (
unique_path,
warn,
watch_logs,
watch_tasks
watch_tasks,
truncate_string
)
try:
@ -254,6 +255,7 @@ def handle_add_host_to_channel(goptions, session, args):
parser = OptionParser(usage=get_usage_str(usage))
parser.add_option("--list", action="store_true", help=SUPPRESS_HELP)
parser.add_option("--new", action="store_true", help=_("Create channel if needed"))
parser.add_option("--force", action="store_true", help=_("force added, if possible"))
(options, args) = parser.parse_args(args)
if not options.list and len(args) != 2:
parser.error(_("Please specify a hostname and a channel"))
@ -267,6 +269,7 @@ def handle_add_host_to_channel(goptions, session, args):
channelinfo = session.getChannel(channel)
if not channelinfo:
error("No such channel: %s" % channel)
host = args[0]
hostinfo = session.getHost(host)
if not hostinfo:
@ -274,6 +277,8 @@ def handle_add_host_to_channel(goptions, session, args):
kwargs = {}
if options.new:
kwargs['create'] = True
if options.force:
kwargs['force'] = True
session.addHostToChannel(host, channel, **kwargs)
@ -348,11 +353,73 @@ def handle_edit_channel(goptions, session, args):
parser = OptionParser(usage=get_usage_str(usage))
parser.add_option("--name", help=_("New channel name"))
parser.add_option("--description", help=_("Description of channel"))
parser.add_option("--comment", help=_("Comment of channel"))
(options, args) = parser.parse_args(args)
if len(args) != 1:
parser.error(_("Incorrect number of arguments"))
activate_session(session, goptions)
session.editChannel(args[0], name=options.name, description=options.description)
vals = {}
for key, val in options.__dict__.items():
if val is not None:
vals[key] = val
cinfo = session.getChannel(args[0])
if not cinfo:
error("No such channel: %s" % args[0])
result = session.editChannel(args[0], **vals)
if not result:
error(_("No changes made, please correct the command line"))
def handle_enable_channel(goptions, session, args):
"[admin] Mark one or more channels as enabled"
usage = _("usage: %prog enable-channel [options] <channelname> [<channelname> ...]")
parser = OptionParser(usage=get_usage_str(usage))
parser.add_option("--comment", help=_("Comment indicating why the channel(s) are being "
"enabled"))
(options, args) = parser.parse_args(args)
if not args:
parser.error(_("At least one channel must be specified"))
activate_session(session, goptions)
with session.multicall() as m:
result = [m.getChannel(channel, strict=False) for channel in args]
error_hit = False
for channel, id in zip(args, result):
if not id.result:
print("No such channel: %s" % channel)
error_hit = True
if error_hit:
error("No changes made. Please correct the command line.")
with session.multicall() as m:
[m.enableChannel(channel, comment=options.comment) for channel in args]
def handle_disable_channel(goptions, session, args):
"[admin] Mark one or more channels as disabled"
usage = _("usage: %prog disable-channel [options] <channelname> [<channelname> ...]")
parser = OptionParser(usage=get_usage_str(usage))
parser.add_option("--comment", help=_("Comment indicating why the channel(s) are being "
"disabled"))
(options, args) = parser.parse_args(args)
if not args:
parser.error(_("At least one channel must be specified"))
activate_session(session, goptions)
with session.multicall() as m:
result = [m.getChannel(channel, strict=False) for channel in args]
error_hit = False
for channel, id in zip(args, result):
if not id.result:
print("No such channel: %s" % channel)
error_hit = True
if error_hit:
error("No changes made. Please correct the command line.")
with session.multicall() as m:
[m.disableChannel(channel, comment=options.comment) for channel in args]
def handle_add_pkg(goptions, session, args):
@ -2872,29 +2939,47 @@ def handle_unblock_group_req(goptions, session, args):
def anon_handle_list_channels(goptions, session, args):
"[info] Print channels listing"
usage = _("usage: %prog list-channels")
usage = _("usage: %prog list-channels [options]")
parser = OptionParser(usage=get_usage_str(usage))
parser.add_option("--simple", action="store_true", default=False,
help=_("Print just list of channels without additional info"))
parser.add_option("--quiet", action="store_true", default=goptions.quiet,
help=_("Do not print header information"))
parser.add_option("--comment", action="store_true", help=_("Show comments"))
parser.add_option("--description", action="store_true", help=_("Show descriptions"))
parser.add_option("--enabled", action="store_true", help=_("Limit to enabled channels"))
parser.add_option("--not-enabled", action="store_false", dest="enabled",
help=_("Limit to not enabled channels"))
parser.add_option("--disabled", action="store_false", dest="enabled",
help=_("Alias for --not-enabled"))
(options, args) = parser.parse_args(args)
ensure_connection(session, goptions)
channels = session.listChannels()
channels = sorted(channels, key=lambda x: x['name'])
opts = {}
if options.enabled is not None:
opts['enabled'] = options.enabled
channels = sorted([x for x in session.listChannels(**opts)], key=lambda x: x['name'])
session.multicall = True
for channel in channels:
session.listHosts(channelID=channel['id'])
for channel, [hosts] in zip(channels, session.multiCall()):
channel['enabled'] = len([x for x in hosts if x['enabled']])
channel['disabled'] = len(hosts) - channel['enabled']
channel['enabled_host'] = len([x for x in hosts if x['enabled']])
channel['disabled'] = len(hosts) - channel['enabled_host']
channel['ready'] = len([x for x in hosts if x['ready']])
channel['capacity'] = sum([x['capacity'] for x in hosts])
channel['load'] = sum([x['task_load'] for x in hosts])
channel['comment'] = truncate_string(channel['comment'])
channel['description'] = truncate_string(channel['description'])
if channel['capacity']:
channel['perc_load'] = channel['load'] / channel['capacity'] * 100.0
else:
channel['perc_load'] = 0.0
if not channel['enabled']:
channel['name'] = channel['name'] + ' [disabled]'
if channels:
longest_channel = max([len(ch['name']) for ch in channels])
else:
longest_channel = 8
if options.simple:
if not options.quiet:
print('Channel')
@ -2902,10 +2987,22 @@ def anon_handle_list_channels(goptions, session, args):
print(channel['name'])
else:
if not options.quiet:
print('Channel Enabled Ready Disbld Load Cap Perc')
hdr = '{channame:<{longest_channel}}Enabled Ready Disbld Load Cap ' \
'Perc '
hdr = hdr.format(longest_channel=longest_channel, channame='Channel')
if options.description:
hdr += "Description".ljust(53)
if options.comment:
hdr += "Comment".ljust(53)
print(hdr)
mask = "%%(name)-%ss %%(enabled_host)6d %%(ready)6d %%(disabled)6d %%(load)6d %%(" \
"capacity)6d %%(perc_load)6d%%%%" % longest_channel
if options.description:
mask += " %(description)-50s"
if options.comment:
mask += " %(comment)-50s"
for channel in channels:
print("%(name)-15s %(enabled)6d %(ready)6d %(disabled)6d %(load)6d %(capacity)6d "
"%(perc_load)6d%%" % channel)
print(mask % channel)
def anon_handle_list_hosts(goptions, session, args):
@ -2954,16 +3051,6 @@ def anon_handle_list_hosts(goptions, session, args):
else:
return 'N'
def truncate(s):
if s:
s = s.replace('\n', ' ')
if len(s) > 47:
return s[:47] + '...'
else:
return s
else:
return ''
try:
first = session.getLastHostUpdate(hosts[0]['id'], ts=True)
opts = {'ts': True}
@ -2985,23 +3072,29 @@ def anon_handle_list_hosts(goptions, session, args):
host['enabled'] = yesno(host['enabled'])
host['ready'] = yesno(host['ready'])
host['arches'] = ','.join(host['arches'].split())
host['description'] = truncate(host['description'])
host['comment'] = truncate(host['comment'])
host['description'] = truncate_string(host['description'])
host['comment'] = truncate_string(host['comment'])
# pull hosts' channels
if options.show_channels:
session.multicall = True
for host in hosts:
session.listChannels(host['id'])
for host, [channels] in zip(hosts, session.multiCall()):
host['channels'] = ','.join(sorted([c['name'] for c in channels]))
with session.multicall() as m:
result = [m.listChannels(host['id']) for host in hosts]
for host, channels in zip(hosts, result):
list_channels = []
for c in channels.result:
if c['enabled']:
list_channels.append(c['name'])
else:
list_channels.append('*' + c['name'])
host['channels'] = ','.join(sorted(list_channels))
if hosts:
longest_host = max([len(h['name']) for h in hosts])
else:
longest_host = 8
if not options.quiet:
hdr = "{hostname:<{longest_host}} Enb Rdy Load/Cap Arches Last Update "
hdr = "{hostname:<{longest_host}} Enb Rdy Load/Cap Arches " \
"Last Update "
hdr = hdr.format(longest_host=longest_host, hostname='Hostname')
if options.description:
hdr += "Description".ljust(51)
@ -3011,7 +3104,7 @@ def anon_handle_list_hosts(goptions, session, args):
hdr += "Channels"
print(hdr)
mask = "%%(name)-%ss %%(enabled)-3s %%(ready)-3s %%(task_load)4.1f/%%(capacity)-4.1f " \
"%%(arches)-16s %%(update)-19s" % longest_host
"%%(arches)-16s %%(update)-35s" % longest_host
if options.description:
mask += " %(description)-50s"
if options.comment:

View file

@ -830,3 +830,15 @@ def format_inheritance_flags(parent):
else:
flags += '.'
return flags
def truncate_string(s, length=47):
"""Return a truncated string when string length is longer than given length."""
if s:
s = s.replace('\n', ' ')
if len(s) > length:
return s[:length] + '...'
else:
return s
else:
return ''

View file

@ -5,5 +5,7 @@
BEGIN;
ALTER TABLE channels ADD COLUMN description TEXT;
ALTER TABLE channels ADD COLUMN enabled BOOLEAN NOT NULL DEFAULT 'true';
ALTER TABLE channels ADD COLUMN comment TEXT;
COMMIT;

View file

@ -136,7 +136,9 @@ CREATE INDEX sessions_expired ON sessions(expired);
CREATE TABLE channels (
id SERIAL NOT NULL PRIMARY KEY,
name VARCHAR(128) UNIQUE NOT NULL,
description TEXT
description TEXT,
enabled BOOLEAN NOT NULL DEFAULT 'true',
comment TEXT
) WITHOUT OIDS;
-- create default channel

View file

@ -2237,7 +2237,7 @@ def set_host_enabled(hostname, enabled=True):
insert.execute()
def add_host_to_channel(hostname, channel_name, create=False):
def add_host_to_channel(hostname, channel_name, create=False, force=False):
"""Add the host to the specified channel
Channel must already exist unless create option is specified
@ -2250,6 +2250,9 @@ def add_host_to_channel(hostname, channel_name, create=False):
channel_id = get_channel_id(channel_name, create=create)
if channel_id is None:
raise koji.GenericError('channel does not exist: %s' % channel_name)
if not force:
if not get_channel(channel_id)['enabled']:
raise koji.GenericError('channel %s is disabled' % channel_name)
channels = list_channels(host_id)
for channel in channels:
if channel['id'] == channel_id:
@ -2307,33 +2310,41 @@ def rename_channel(old, new):
update.execute()
def edit_channel(channelInfo, name=None, description=None):
def edit_channel(channelInfo, **kw):
"""Edit information for an existing channel.
:param str/int channelInfo: channel name or ID
:param str name: new channel name
:param str description: description of channel
:param str comment: comment about channel
"""
context.session.assertPerm('admin')
channel = get_channel(channelInfo, strict=True)
if name:
if not isinstance(name, str):
fields = ('description', 'comment', 'name')
changes = []
for field in fields:
if field in kw and kw[field] != channel[field]:
changes.append(field)
if not changes:
return False
if kw.get('name'):
if not isinstance(kw['name'], str):
raise koji.GenericError("new channel name must be a string")
dup_check = get_channel(name, strict=False)
dup_check = get_channel(kw['name'], strict=False)
if dup_check:
raise koji.GenericError("channel %(name)s already exists (id=%(id)i)" % dup_check)
update = UpdateProcessor('channels',
values={'channelID': channel['id']},
clauses=['id = %(channelID)i'])
if name:
update.set(name=name)
if description:
update.set(description=description)
for change in changes:
update.set(**{change: kw[change]})
update.execute()
return None
return True
def remove_channel(channel_name, force=False):
@ -2388,6 +2399,18 @@ def add_channel(channel_name, description=None):
return channel_id
def set_channel_enabled(channelname, enabled=True, comment=None):
context.session.assertPerm('host')
channel = get_channel(channelname)
if not channel:
raise koji.GenericError('No such channel: %s' % channelname)
update = UpdateProcessor('channels', values=channel, clauses=['id = %(id)i'])
update.set(enabled=enabled)
if comment is not None:
update.set(comment=comment)
update.execute()
def get_ready_hosts():
"""Return information about hosts that are ready to build.
@ -5372,7 +5395,7 @@ def get_channel(channelInfo, strict=False):
:returns: dict of the channel ID and name, or None.
For example, {'id': 20, 'name': 'container'}
"""
fields = ('id', 'name', 'description')
fields = ('id', 'name', 'description', 'enabled', 'comment')
query = """SELECT %s FROM channels
WHERE """ % ', '.join(fields)
if isinstance(channelInfo, int):
@ -5514,7 +5537,7 @@ def get_buildroot(buildrootID, strict=False):
return result[0]
def list_channels(hostID=None, event=None):
def list_channels(hostID=None, event=None, enabled=None):
"""
List builder channels.
@ -5526,18 +5549,29 @@ def list_channels(hostID=None, event=None):
default behavior is to search for the "active" host
settings. You must specify a hostID parameter with this
option.
:param bool enabled: Enabled/disabled list of channels
:returns: list of dicts, one per channel. For example,
[{'id': 20, 'name': 'container', 'description': 'container channel'}]
[{'comment': 'test channel', 'description': 'container channel',
'enabled': True, 'id': 20, 'name': 'container', 'container channel' }]
"""
fields = {'channels.id': 'id', 'channels.name': 'name',
'channels.description': 'description'}
fields = {'channels.id': 'id', 'channels.name': 'name', 'channels.description': 'description',
'channels.enabled': 'enabled', 'channels.comment': 'comment'}
columns, aliases = zip(*fields.items())
if enabled is not None:
if enabled:
enable_clause = 'enabled IS TRUE'
else:
enable_clause = 'enabled IS FALSE'
if hostID:
if isinstance(hostID, str):
hostID = get_host(hostID, strict=True)['id']
tables = ['host_channels']
joins = ['channels ON channels.id = host_channels.channel_id']
clauses = [
eventCondition(event, table='host_channels'),
'host_channels.host_id = %(host_id)s']
if enabled is not None:
clauses.append(enable_clause)
values = {'host_id': hostID}
query = QueryProcessor(tables=tables, aliases=aliases,
columns=columns, joins=joins,
@ -5546,8 +5580,12 @@ def list_channels(hostID=None, event=None):
raise koji.GenericError('list_channels with event and '
'not host is not allowed.')
else:
if enabled is not None:
clauses = [enable_clause]
else:
clauses = None
query = QueryProcessor(tables=['channels'], aliases=aliases,
columns=columns)
columns=columns, clauses=clauses)
return query.execute()
@ -12643,6 +12681,14 @@ class RootExports(object):
"""Mark a host as disabled"""
set_host_enabled(hostname, False)
def enableChannel(self, channelname, comment=None):
"""Mark a channel as enabled"""
set_channel_enabled(channelname, enabled=True, comment=comment)
def disableChannel(self, channelname, comment=None):
"""Mark a channel as disabled"""
set_channel_enabled(channelname, enabled=False, comment=comment)
getHost = staticmethod(get_host)
editHost = staticmethod(edit_host)
addHostToChannel = staticmethod(add_host_to_channel)

View file

@ -20,6 +20,7 @@ admin commands:
block-group-req Block a group's requirement listing
block-pkg Block a package in the listing for tag
clone-tag Duplicate the contents of one tag onto another tag
disable-channel Mark one or more channels as disabled
disable-host Mark one or more hosts as disabled
disable-user Disable logins by a user
edit-channel Edit a channel
@ -29,6 +30,7 @@ admin commands:
edit-tag-inheritance Edit tag inheritance
edit-target Set the name, build_tag, and/or dest_tag of an existing build target to new values
edit-user Alter user information
enable-channel Mark one or more channels as enabled
enable-host Mark one or more hosts as enabled
enable-user Enable logins by a user
free-task Free a task

View file

@ -20,6 +20,7 @@ admin commands:
block-group-req Block a group's requirement listing
block-pkg Block a package in the listing for tag
clone-tag Duplicate the contents of one tag onto another tag
disable-channel Mark one or more channels as disabled
disable-host Mark one or more hosts as disabled
disable-user Disable logins by a user
edit-channel Edit a channel
@ -29,6 +30,7 @@ admin commands:
edit-tag-inheritance Edit tag inheritance
edit-target Set the name, build_tag, and/or dest_tag of an existing build target to new values
edit-user Alter user information
enable-channel Mark one or more channels as enabled
enable-host Mark one or more hosts as enabled
enable-user Enable logins by a user
free-task Free a task

View file

@ -9,6 +9,7 @@ import unittest
from koji_cli.commands import handle_add_host_to_channel
from . import utils
class TestAddHostToChannel(utils.CliTestCase):
# Show long diffs in error output...
@ -80,7 +81,6 @@ class TestAddHostToChannel(utils.CliTestCase):
channel = 'channel'
new_arg = '--new'
args = [host, channel, new_arg]
kwargs = {'create': True}
options = mock.MagicMock()
# Mock out the xmlrpc server
@ -98,8 +98,7 @@ class TestAddHostToChannel(utils.CliTestCase):
activate_session_mock.assert_called_once_with(session, options)
session.getChannel.assert_not_called()
session.getHost.assert_called_once_with(host)
session.addHostToChannel.assert_called_once_with(
host, channel, **kwargs)
session.addHostToChannel.assert_called_once_with(host, channel, create=True)
self.assertNotEqual(rv, 1)
@mock.patch('sys.stderr', new_callable=six.StringIO)

View file

@ -0,0 +1,115 @@
from __future__ import absolute_import
import unittest
import mock
import six
import koji
from koji_cli.commands import handle_disable_channel
from . import utils
class TestDisableChannel(utils.CliTestCase):
# Show long diffs in error output...
maxDiff = None
def setUp(self):
self.error_format = """Usage: %s disable-channel [options] <channelname> [<channelname> ...]
(Specify the --help global option for a list of other help options)
%s: error: {message}
""" % (self.progname, self.progname)
self.channelinfo = [
{'comment': None, 'description': None, 'enabled': False, 'id': 1,
'name': 'test-channel'}
]
def __vm(self, result):
m = koji.VirtualCall('mcall_method', [], {})
if isinstance(result, dict) and result.get('faultCode'):
m._result = result
else:
m._result = (result,)
return m
@mock.patch('sys.stderr', new_callable=six.StringIO)
@mock.patch('sys.stdout', new_callable=six.StringIO)
@mock.patch('koji_cli.commands.activate_session')
def test_handle_disable_channel(self, activate_session_mock, stdout, stderr):
"""Test disable-channel function"""
options = mock.MagicMock()
session = mock.MagicMock()
mcall = session.multicall.return_value.__enter__.return_value
mcall.getChannel.return_value = self.__vm(None)
arguments = ['test-channel']
with self.assertRaises(SystemExit) as ex:
handle_disable_channel(options, session, arguments)
self.assertExitCode(ex, 1)
activate_session_mock.assert_called_once()
session.multicall.assert_called_once()
session.disableChannel.assert_not_called()
expect = ''
for host in arguments:
expect += "No such channel: %s\n" % host
stderr_exp = "No changes made. Please correct the command line.\n"
self.assert_console_message(stdout, expect)
self.assert_console_message(stderr, stderr_exp)
# reset session mocks
activate_session_mock.reset_mock()
session.disableChannel.reset_mock()
session.multicall.reset_mock()
mcall = session.multicall.return_value.__enter__.return_value
mcall.getChannel.return_value = self.__vm(self.channelinfo)
arguments = ['test-channel', '--comment', 'enable channel test']
handle_disable_channel(options, session, arguments)
activate_session_mock.assert_called_once()
self.assertEqual(2, session.multicall.call_count)
self.assert_console_message(stdout, '')
@mock.patch('sys.stdout', new_callable=six.StringIO)
@mock.patch('koji_cli.commands.activate_session')
def test_handle_disable_host_no_argument(self, activate_session_mock, stdout):
"""Test disable-channel function without arguments"""
options = mock.MagicMock()
session = mock.MagicMock()
session.getChannel.return_value = None
session.multicall.return_value = [[None]]
session.disableChannel.return_value = True
expected = self.format_error_message("At least one channel must be specified")
self.assert_system_exit(
handle_disable_channel,
options,
session,
[],
stderr=expected,
activate_session=None)
activate_session_mock.assert_not_called()
session.getChannel.assert_not_called()
session.multicall.assert_not_called()
session.disableChannel.assert_not_called()
def test_handle_disable_channel_help(self):
"""Test disable-channel help message"""
self.assert_help(
handle_disable_channel,
"""Usage: %s disable-channel [options] <channelname> [<channelname> ...]
(Specify the --help global option for a list of other help options)
Options:
-h, --help show this help message and exit
--comment=COMMENT Comment indicating why the channel(s) are being disabled
""" % self.progname)
if __name__ == '__main__':
unittest.main()

View file

@ -34,6 +34,7 @@ Options:
--name=NAME New channel name
--description=DESCRIPTION
Description of channel
--comment=COMMENT Comment of channel
""" % self.progname)
@mock.patch('sys.stderr', new_callable=six.StringIO)

View file

@ -0,0 +1,115 @@
from __future__ import absolute_import
import unittest
import mock
import six
import koji
from koji_cli.commands import handle_enable_channel
from . import utils
class TestEnableChannel(utils.CliTestCase):
# Show long diffs in error output...
maxDiff = None
def setUp(self):
self.error_format = """Usage: %s enable-channel [options] <channelname> [<channelname> ...]
(Specify the --help global option for a list of other help options)
%s: error: {message}
""" % (self.progname, self.progname)
self.channelinfo = [
{'comment': None, 'description': None, 'enabled': False, 'id': 1,
'name': 'test-channel'}
]
def __vm(self, result):
m = koji.VirtualCall('mcall_method', [], {})
if isinstance(result, dict) and result.get('faultCode'):
m._result = result
else:
m._result = (result,)
return m
@mock.patch('sys.stderr', new_callable=six.StringIO)
@mock.patch('sys.stdout', new_callable=six.StringIO)
@mock.patch('koji_cli.commands.activate_session')
def test_handle_enable_channel(self, activate_session_mock, stdout, stderr):
"""Test enable-channel function"""
options = mock.MagicMock()
session = mock.MagicMock()
mcall = session.multicall.return_value.__enter__.return_value
mcall.getChannel.return_value = self.__vm(None)
arguments = ['channel1', 'channel2']
with self.assertRaises(SystemExit) as ex:
handle_enable_channel(options, session, arguments)
self.assertExitCode(ex, 1)
activate_session_mock.assert_called_once()
session.multicall.assert_called_once()
session.enableChannel.assert_not_called()
expect = ''
for host in arguments:
expect += "No such channel: %s\n" % host
stderr_exp = "No changes made. Please correct the command line.\n"
self.assert_console_message(stdout, expect)
self.assert_console_message(stderr, stderr_exp)
# reset session mocks
activate_session_mock.reset_mock()
session.multicall.reset_mock()
session.enableChannel.reset_mock()
mcall = session.multicall.return_value.__enter__.return_value
mcall.getChannel.return_value = self.__vm(self.channelinfo)
arguments = ['channel1', 'channel2', '--comment', 'enable channel test']
handle_enable_channel(options, session, arguments)
activate_session_mock.assert_called_once()
self.assertEqual(2, session.multicall.call_count)
self.assert_console_message(stdout, '')
@mock.patch('sys.stdout', new_callable=six.StringIO)
@mock.patch('koji_cli.commands.activate_session')
def test_handle_enable_host_no_argument(self, activate_session_mock, stdout):
"""Test enable-channel function without arguments"""
options = mock.MagicMock()
session = mock.MagicMock()
session.getChannel.return_value = None
session.multicall.return_value = [[None]]
session.enableChannel.return_value = True
expected = self.format_error_message("At least one channel must be specified")
self.assert_system_exit(
handle_enable_channel,
options,
session,
[],
stderr=expected,
activate_session=None)
activate_session_mock.assert_not_called()
session.getChannel.assert_not_called()
session.multicall.assert_not_called()
session.enableChannel.assert_not_called()
def test_handle_enable_channel_help(self):
"""Test enable-channel help message"""
self.assert_help(
handle_enable_channel,
"""Usage: %s enable-channel [options] <channelname> [<channelname> ...]
(Specify the --help global option for a list of other help options)
Options:
-h, --help show this help message and exit
--comment=COMMENT Comment indicating why the channel(s) are being enabled
""" % self.progname)
if __name__ == '__main__':
unittest.main()

View file

@ -1,28 +1,30 @@
from __future__ import absolute_import
import mock
import unittest
import mock
from six.moves import StringIO
import koji
from koji_cli.commands import anon_handle_list_channels
from . import utils
class TestListChannels(utils.CliTestCase):
maxDiff = None
class TestListChannels(unittest.TestCase):
def setUp(self):
self.options = mock.MagicMock()
self.options.quiet = True
self.session = mock.MagicMock()
self.session.getAPIVersion.return_value = koji.API_VERSION
self.args = []
@mock.patch('sys.stdout', new_callable=StringIO)
@mock.patch('koji_cli.commands.ensure_connection')
def test_list_channels(self, ensure_connection_mock, stdout):
self.session.listChannels.return_value = [
{'id': 1, 'name': 'default'},
{'id': 2, 'name': 'test'},
self.list_channels = [
{'id': 1, 'name': 'default', 'enabled': True, 'comment': 'test-comment-1',
'description': 'test-description-1'},
{'id': 2, 'name': 'test', 'enabled': False, 'comment': 'test-comment-2',
'description': 'test-description-2'},
]
self.session.multiCall.return_value = [
self.list_hosts_mc = [
[[
{'enabled': True, 'ready': True, 'capacity': 2.0, 'task_load': 1.34},
{'enabled': True, 'ready': False, 'capacity': 2.0, 'task_load': 0.0},
@ -32,16 +34,78 @@ class TestListChannels(unittest.TestCase):
{'enabled': True, 'ready': True, 'capacity': 2.0, 'task_load': 1.34},
{'enabled': False, 'ready': True, 'capacity': 2.0, 'task_load': 0.34},
{'enabled': True, 'ready': False, 'capacity': 2.0, 'task_load': 0.0},
]],
]]
]
anon_handle_list_channels(self.options, self.session, self.args)
@mock.patch('sys.stdout', new_callable=StringIO)
@mock.patch('koji_cli.commands.ensure_connection')
def test_list_channels(self, ensure_connection_mock, stdout):
self.session.listChannels.return_value = self.list_channels
self.session.multiCall.return_value = self.list_hosts_mc
args = []
anon_handle_list_channels(self.options, self.session, args)
actual = stdout.getvalue()
print(actual)
expected = """\
default 3 1 0 1 6 22%
test 2 2 1 1 6 28%
test [disabled] 2 2 1 1 6 28%
"""
self.assertMultiLineEqual(actual, expected)
ensure_connection_mock.assert_called_once_with(self.session, self.options)
@mock.patch('sys.stdout', new_callable=StringIO)
@mock.patch('koji_cli.commands.ensure_connection')
def test_list_channels_with_comment(self, ensure_connection_mock, stdout):
self.session.listChannels.return_value = self.list_channels
self.session.multiCall.return_value = self.list_hosts_mc
args = ['--comment']
anon_handle_list_channels(self.options, self.session, args)
actual = stdout.getvalue()
print(actual)
expected = 'default 3 1 0 1 6 22% ' \
'test-comment-1 \n' \
'test [disabled] 2 2 1 1 6 28% ' \
'test-comment-2 \n'
self.assertMultiLineEqual(actual, expected)
ensure_connection_mock.assert_called_once_with(self.session, self.options)
@mock.patch('sys.stdout', new_callable=StringIO)
@mock.patch('koji_cli.commands.ensure_connection')
def test_list_channels_with_description(self, ensure_connection_mock, stdout):
self.session.listChannels.return_value = self.list_channels
self.session.multiCall.return_value = self.list_hosts_mc
args = ['--description']
anon_handle_list_channels(self.options, self.session, args)
actual = stdout.getvalue()
print(actual)
expected = 'default 3 1 0 1 6 22% ' \
'test-description-1 \n' \
'test [disabled] 2 2 1 1 6 28% ' \
'test-description-2 \n'
self.assertMultiLineEqual(actual, expected)
ensure_connection_mock.assert_called_once_with(self.session, self.options)
def test_list_channels_help(self):
self.assert_help(
anon_handle_list_channels,
"""Usage: %s list-channels [options]
(Specify the --help global option for a list of other help options)
Options:
-h, --help show this help message and exit
--simple Print just list of channels without additional info
--quiet Do not print header information
--comment Show comments
--description Show descriptions
--enabled Limit to enabled channels
--not-enabled Limit to not enabled channels
--disabled Alias for --not-enabled
""" % self.progname)
if __name__ == '__main__':
unittest.main()

View file

@ -34,8 +34,8 @@ class TestListHosts(utils.CliTestCase):
@mock.patch('koji_cli.commands.ensure_connection')
def test_list_hosts_valid(self, ensure_connection, stdout):
host_update = 1615875554.862938
expected = """kojibuilder Y Y 0.0/2.0 x86_64 Tue, 16 Mar 2021 06:19:14 UTC
"""
expected = "kojibuilder Y Y 0.0/2.0 x86_64 " \
"Tue, 16 Mar 2021 06:19:14 UTC \n"
list_hosts = [{'arches': 'x86_64',
'capacity': 2.0,
'comment': None,

View file

@ -31,19 +31,22 @@ class TestAddHostToChannel(unittest.TestCase):
def tearDown(self):
mock.patch.stopall()
@mock.patch('kojihub.get_channel')
@mock.patch('kojihub.list_channels')
@mock.patch('kojihub.get_channel_id')
@mock.patch('kojihub.get_host')
def test_valid(self, get_host, get_channel_id, list_channels):
def test_valid(self, get_host, get_channel_id, list_channels, get_channel):
name = 'hostname'
cname = 'channel_name'
get_host.return_value = {'id': 123, 'name': name}
get_channel_id.return_value = 456
list_channels.return_value = [{'id': 1, 'name': 'default'}]
get_channel.return_value = {'enabled': True}
kojihub.add_host_to_channel(name, cname, create=False)
get_host.assert_called_once_with(name)
get_channel.assert_called_once_with(456)
get_channel_id.assert_called_once_with(cname, create=False)
list_channels.assert_called_once_with(123)
@ -88,19 +91,22 @@ class TestAddHostToChannel(unittest.TestCase):
get_channel_id.assert_called_once_with(cname, create=False)
self.assertEqual(len(self.inserts), 0)
@mock.patch('kojihub.get_channel')
@mock.patch('kojihub.list_channels')
@mock.patch('kojihub.get_channel_id')
@mock.patch('kojihub.get_host')
def test_no_channel_create(self, get_host, get_channel_id, list_channels):
def test_no_channel_create(self, get_host, get_channel_id, list_channels, get_channel):
name = 'hostname'
cname = 'channel_name'
get_host.return_value = {'id': 123, 'name': name}
get_channel_id.return_value = 456
list_channels.return_value = [{'id': 1, 'name': 'default'}]
get_channel.return_value = {'enabled': True}
kojihub.add_host_to_channel(name, cname, create=True)
get_host.assert_called_once_with(name)
get_channel.assert_called_once_with(456)
get_channel_id.assert_called_once_with(cname, create=True)
list_channels.assert_called_once_with(123)
@ -116,20 +122,23 @@ class TestAddHostToChannel(unittest.TestCase):
self.assertEqual(insert.data, data)
self.assertEqual(insert.rawdata, {})
@mock.patch('kojihub.get_channel')
@mock.patch('kojihub.list_channels')
@mock.patch('kojihub.get_channel_id')
@mock.patch('kojihub.get_host')
def test_exists(self, get_host, get_channel_id, list_channels):
def test_exists(self, get_host, get_channel_id, list_channels, get_channel):
name = 'hostname'
cname = 'channel_name'
get_host.return_value = {'id': 123, 'name': name}
get_channel_id.return_value = 456
list_channels.return_value = [{'id': 456, 'name': cname}]
get_channel.return_value = {'enabled': True}
with self.assertRaises(koji.GenericError):
kojihub.add_host_to_channel(name, cname, create=False)
get_host.assert_called_once_with(name)
get_channel.assert_called_once_with(456)
get_channel_id.assert_called_once_with(cname, create=False)
list_channels.assert_called_once_with(123)
self.assertEqual(len(self.inserts), 0)

View file

@ -0,0 +1,43 @@
import unittest
import mock
import koji
import kojihub
UP = kojihub.UpdateProcessor
class TestDisableChannel(unittest.TestCase):
def getUpdate(self, *args, **kwargs):
update = UP(*args, **kwargs)
update.execute = mock.MagicMock()
self.updates.append(update)
return update
def setUp(self):
self.exports = kojihub.RootExports()
self.get_channel = mock.patch('kojihub.get_channel').start()
self.UpdateProcessor = mock.patch('kojihub.UpdateProcessor',
side_effect=self.getUpdate).start()
self.updates = []
self.channelname = 'test-channel'
def test_non_exist_channel(self):
self.get_channel.return_value = None
with self.assertRaises(koji.GenericError) as cm:
self.exports.disableChannel(self.channelname)
self.assertEqual("No such channel: %s" % self.channelname, str(cm.exception))
def test_valid(self):
self.get_channel.return_value = {'comment': None, 'description': None,
'enabled': True, 'id': 1, 'name': 'test-channel'}
self.exports.disableChannel(self.channelname, comment='test-comment')
self.assertEqual(len(self.updates), 1)
update = self.updates[0]
self.assertEqual(update.table, 'channels')
self.assertEqual(update.data, {'comment': 'test-comment', 'enabled': False})
self.assertEqual(update.values, {'comment': None, 'description': None, 'enabled': True,
'id': 1, 'name': 'test-channel'})
self.assertEqual(update.clauses, ['id = %(id)i'])

View file

@ -84,7 +84,7 @@ class TestEditChannel(unittest.TestCase):
r = self.exports.editChannel(self.channel_name, name=self.channel_name_new,
description='description_new')
self.assertIsNone(r)
self.assertTrue(r)
expected_calls = [mock.call(self.channel_name, strict=True),
mock.call(self.channel_name_new, strict=False)]
get_channel.assert_has_calls(expected_calls)

View file

@ -0,0 +1,44 @@
import unittest
import mock
import koji
import kojihub
UP = kojihub.UpdateProcessor
class TestEnableChannel(unittest.TestCase):
def getUpdate(self, *args, **kwargs):
update = UP(*args, **kwargs)
update.execute = mock.MagicMock()
self.updates.append(update)
return update
def setUp(self):
self.exports = kojihub.RootExports()
self.get_channel = mock.patch('kojihub.get_channel').start()
self.UpdateProcessor = mock.patch('kojihub.UpdateProcessor',
side_effect=self.getUpdate).start()
self.updates = []
self.channelname = 'test-channel'
def test_non_exist_channel(self):
self.get_channel.return_value = None
with self.assertRaises(koji.GenericError) as cm:
self.exports.enableChannel(self.channelname)
self.assertEqual("No such channel: %s" % self.channelname, str(cm.exception))
def test_valid(self):
self.get_channel.return_value = {'comment': None, 'description': None,
'enabled': False, 'id': 1, 'name': 'test-channel'}
self.exports.enableChannel(self.channelname, comment='test-comment')
self.assertEqual(len(self.updates), 1)
update = self.updates[0]
self.assertEqual(update.table, 'channels')
self.assertEqual(update.data, {'comment': 'test-comment', 'enabled': True})
self.assertEqual(update.values, {'comment': None, 'description': None, 'enabled': False,
'id': 1, 'name': 'test-channel'})
self.assertEqual(update.clauses, ['id = %(id)i'])

View file

@ -33,10 +33,12 @@ class TestListChannels(unittest.TestCase):
self.assertEqual(len(self.queries), 1)
query = self.queries[0]
self.assertEqual(query.tables, ['channels'])
self.assertEqual(query.aliases, ['description', 'id', 'name'])
self.assertEqual(query.aliases, ['comment', 'description', 'enabled', 'id',
'name'])
self.assertEqual(query.joins, None)
self.assertEqual(query.values, {})
self.assertEqual(query.columns, ['channels.description', 'channels.id', 'channels.name'])
self.assertEqual(query.columns, ['channels.comment', 'channels.description',
'channels.enabled', 'channels.id', 'channels.name'])
self.assertEqual(query.clauses, None)
def test_host(self):
@ -50,10 +52,12 @@ class TestListChannels(unittest.TestCase):
'host_channels.host_id = %(host_id)s'
]
self.assertEqual(query.tables, ['host_channels'])
self.assertEqual(query.aliases, ['description', 'id', 'name'])
self.assertEqual(query.aliases, ['comment', 'description', 'enabled', 'id',
'name'])
self.assertEqual(query.joins, joins)
self.assertEqual(query.values, {'host_id': 1234})
self.assertEqual(query.columns, ['channels.description', 'channels.id', 'channels.name'])
self.assertEqual(query.columns, ['channels.comment', 'channels.description',
'channels.enabled', 'channels.id', 'channels.name'])
self.assertEqual(query.clauses, clauses)
def test_host_and_event(self):
@ -68,10 +72,12 @@ class TestListChannels(unittest.TestCase):
'host_channels.host_id = %(host_id)s',
]
self.assertEqual(query.tables, ['host_channels'])
self.assertEqual(query.aliases, ['description', 'id', 'name'])
self.assertEqual(query.aliases, ['comment', 'description', 'enabled', 'id',
'name'])
self.assertEqual(query.joins, joins)
self.assertEqual(query.values, {'host_id': 1234})
self.assertEqual(query.columns, ['channels.description', 'channels.id', 'channels.name'])
self.assertEqual(query.columns, ['channels.comment', 'channels.description',
'channels.enabled', 'channels.id', 'channels.name'])
self.assertEqual(query.clauses, clauses)
def test_event_only(self):

View file

@ -6,7 +6,7 @@
<table>
<tr>
<th>Name</th><td>$channel.name</td>
<th>Name</th><td>$util.escapeHTML($channel.name)</td>
</tr>
<tr>
<th>ID</th><td>$channel.id</td>
@ -14,6 +14,16 @@
<tr>
<th>Description</th><td>$util.escapeHTML($channel.description)</td>
</tr>
<tr>
#set $enabled = $channel.enabled and 'yes' or 'no'
<th>Enabled?</th>
<td class="$enabled">
$util.imageTag($enabled)
</td>
</tr>
<tr>
<th>Comment</th><td>$util.escapeHTML($channel.comment)</td>
</tr>
<tr>
<th>Active Tasks</th><td><a href="tasks?view=flat&channelID=$channel.id">$taskCount</a></td>
</tr>

View file

@ -60,6 +60,9 @@
<tr>
<th>
<a href="channelinfo?channelID=$channel['id']">$channel['name']</a>
#if not $channel['enabled_channel']
[disabled]
#end if
</th>
<td width="$graphWidth" class="graph">
#if $channel['capacityPerc']

View file

@ -51,7 +51,7 @@
<th>Channels</th>
<td>
#for $channel in $channels
<a href="channelinfo?channelID=$channel.id">$channel.name</a><br/>
<a href="channelinfo?channelID=$channel.id" class="$channel.enabled">$channel.name</a><br/>
#end for
#if not $channels
No channels

View file

@ -123,8 +123,8 @@ in $channel channel
<td><a href="hostinfo?hostID=$host.id">$host.name</a></td>
<td>$host.arches</td>
<td>
#for $channame, $chan_id in zip($host.channels, $host.channels_id)
<a href="channelinfo?channelID=$chan_id">$channame</a>
#for $channame, $chan_id, $chan_enabled in zip($host.channels, $host.channels_id, $host.channels_enabled)
<a href="channelinfo?channelID=$chan_id" class="$chan_enabled">$channame</a>
#end for
</td>
<td class="$str($bool($host.enabled)).lower()">#if $host.enabled then $util.imageTag('yes') else $util.imageTag('no')#</td>

View file

@ -1661,9 +1661,14 @@ def hosts(environ, state='enabled', start=None, order='name', ready='all', chann
for host, channels in zip(hosts, list_channels):
host['channels'] = []
host['channels_id'] = []
host['channels_enabled'] = []
for chan in channels.result:
host['channels'].append(chan['name'])
host['channels_id'].append(chan['id'])
if chan['enabled']:
host['channels_enabled'].append('enabled')
else:
host['channels_enabled'].append('disabled')
if channel != 'all':
hosts = [x for x in hosts if channel in x['channels']]
@ -1709,6 +1714,11 @@ def hostinfo(environ, hostID=None, userID=None):
channels = server.listChannels(host['id'])
channels.sort(key=_sortbyname)
for chan in channels:
if chan['enabled']:
chan['enabled'] = 'enabled'
else:
chan['enabled'] = 'disabled'
buildroots = server.listBuildroots(hostID=host['id'],
state=[state[1] for state in koji.BR_STATES.items()
if state[0] != 'EXPIRED'])
@ -2359,6 +2369,7 @@ def clusterhealth(environ, arch='__all__'):
for host in hosts:
arches |= set(host['arches'].split())
hosts = _filter_hosts_by_arch(hosts, arch)
channel['enabled_channel'] = channel['enabled']
channel['enabled'] = len([x for x in hosts if x['enabled']])
channel['disabled'] = len(hosts) - channel['enabled']
channel['ready'] = len([x for x in hosts if x['ready']])