#2933 Enable/disable channel
Merged 10 months ago by tkopecek. Opened 11 months ago by jcupova.
jcupova/koji issue-1851  into  master

file modified
+122 -29
@@ -45,7 +45,8 @@ 

      unique_path,

      warn,

      watch_logs,

-     watch_tasks

+     watch_tasks,

+     truncate_string

  )

  

  try:
@@ -254,6 +255,7 @@ 

      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 @@ 

          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 @@ 

      kwargs = {}

      if options.new:

          kwargs['create'] = True

+     if options.force:

+         kwargs['force'] = True

      session.addHostToChannel(host, channel, **kwargs)

  

  
@@ -348,11 +353,73 @@ 

      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 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 @@ 

              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 @@ 

          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 @@ 

          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 @@ 

              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:

file modified
+12
@@ -830,3 +830,15 @@ 

          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 ''

@@ -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;

file modified
+3 -1
@@ -136,7 +136,9 @@ 

  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

file modified
+62 -16
@@ -2236,7 +2236,7 @@ 

      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
@@ -2249,6 +2249,9 @@ 

      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:
@@ -2306,33 +2309,41 @@ 

      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):
@@ -2387,6 +2398,18 @@ 

      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.

  
@@ -5371,7 +5394,7 @@ 

      :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):
@@ -5513,7 +5536,7 @@ 

      return result[0]

  

  

- def list_channels(hostID=None, event=None):

+ def list_channels(hostID=None, event=None, enabled=None):

      """

      List builder channels.

  
@@ -5525,18 +5548,29 @@ 

                        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,
@@ -5545,8 +5579,12 @@ 

          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()

  

  
@@ -12638,6 +12676,14 @@ 

          """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)

@@ -20,6 +20,7 @@ 

          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 @@ 

          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

@@ -20,6 +20,7 @@ 

          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 @@ 

          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

@@ -9,6 +9,7 @@ 

  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 @@ 

          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 @@ 

          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)

@@ -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()

@@ -34,6 +34,7 @@ 

    --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)

@@ -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()

@@ -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 @@ 

                  {'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()

@@ -34,8 +34,8 @@ 

      @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,

@@ -31,19 +31,22 @@ 

      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 @@ 

          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 @@ 

          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)

@@ -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'])

@@ -84,7 +84,7 @@ 

  

          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)

@@ -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'])

@@ -33,10 +33,12 @@ 

          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 @@ 

              '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 @@ 

              '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):

file modified
+11 -1
@@ -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>
@@ -15,6 +15,16 @@ 

        <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>

      <tr>

@@ -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']

file modified
+1 -1
@@ -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

file modified
+2 -2
@@ -123,8 +123,8 @@ 

            <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>

file modified
+11
@@ -1661,9 +1661,14 @@ 

      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 @@ 

  

      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 @@ 

          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']])

rebased onto 0b6b048ac0dde810da5aeaf07282a28b875264ee

11 months ago

rebased onto 5d0cf4f18b3fe76b532d5690a1658748617164fe

10 months ago

If comment is not specified, previous comment should stay in place. With this code it will be overwritten with empty string (same for disable) (wrong place for my comment - meant to be in final call of handle_enable_channel)

It is copy from list-hosts. Let's move it to koji_cli/lib.py (and add option for length with 50 default (full length))

rebased onto 3047f997be98cba52c14851fe5c1591f184c2cb5

10 months ago

koji edit-channel default (without any options) will raise a traceback

typo here: s/description/comment/

Thins needs to be length - 3 (resulting string with ... has to be == length)

rebased onto df4adbfe68d5d1d55fccf5c84c3878d73cbf760a

10 months ago

Metadata Update from @julian8628:
- Pull-request tagged with: testing-ready

10 months ago

rebased onto 9c42b942db48d92719646c4a36c0e11336786014

10 months ago

rebased onto 86aba5264c3f2a27b4d99dabdcf1f634616cd317

10 months ago

rebased onto 53773560d48e4eef887ce84553a4cac3f4354d08

10 months ago

rebased onto 262f8e2d5a3f1dbfc98076f716e0b718f50f850b

10 months ago

rebased onto 17fe5c6

10 months ago

Metadata Update from @jobrauer:
- Pull-request tagged with: testing-done

10 months ago

Commit cfcf900 fixes this pull-request

Pull-Request has been merged by tkopecek

10 months ago