diff --git a/cli/koji_cli/commands.py b/cli/koji_cli/commands.py index df63208c..32c040d4 100644 --- a/cli/koji_cli/commands.py +++ b/cli/koji_cli/commands.py @@ -2772,9 +2772,9 @@ def anon_handle_list_hosts(goptions, session, args): host['arches'] = ','.join(host['arches'].split()) if not options.quiet: - print("Hostname Enb Rdy Load/Cap Arches Last Update") + print("Hostname Enb Rdy Load/Cap Arches Last Update") for host in hosts: - print("%(name)-28s %(enabled)-3s %(ready)-3s %(task_load)4.1f/%(capacity)-3.1f %(arches)-16s %(update)s" % host) + print("%(name)-28s %(enabled)-3s %(ready)-3s %(task_load)4.1f/%(capacity)-4.1f %(arches)-16s %(update)s" % host) def anon_handle_list_pkgs(goptions, session, args): @@ -4027,6 +4027,13 @@ def _print_histline(entry, **kwargs): fmt = "added tag option %(key)s for tag %(tag.name)s" else: fmt = "tag option %(key)s removed for %(tag.name)s" + elif table == 'host_config': + if edit: + fmt = "host configuration for %(host.name)s altered" + elif create: + fmt = "new host: %(host.name)s" + else: + fmt = "host deleted: %(host.name)s" elif table == 'build_target_config': if edit: fmt = "build target configuration for %(build_target.name)s updated" @@ -4141,6 +4148,7 @@ _table_keys = { 'tag_extra' : ['tag_id', 'key'], 'build_target_config' : ['build_target_id'], 'external_repo_config' : ['external_repo_id'], + 'host_config': ['host_id'], 'tag_external_repos' : ['tag_id', 'external_repo_id'], 'tag_listing' : ['build_id', 'tag_id'], 'tag_packages' : ['package_id', 'tag_id'], diff --git a/docs/schema-upgrade-1.15-1.16.sql b/docs/schema-upgrade-1.15-1.16.sql new file mode 100644 index 00000000..e4fa6e2d --- /dev/null +++ b/docs/schema-upgrade-1.15-1.16.sql @@ -0,0 +1,47 @@ +-- upgrade script to migrate the Koji database schema +-- from version 1.13 to 1.14 + + +BEGIN; + +-- create host_config table +SELECT 'Creating table host_config'; +CREATE TABLE host_config ( + host_id INTEGER NOT NULL REFERENCES host(id), + arches TEXT, + capacity FLOAT CHECK (capacity > 1) NOT NULL DEFAULT 2.0, + description TEXT, + comment TEXT, + enabled BOOLEAN NOT NULL DEFAULT 'true', +-- versioned - see desc above + create_event INTEGER NOT NULL REFERENCES events(id) DEFAULT get_event(), + revoke_event INTEGER REFERENCES events(id), + creator_id INTEGER NOT NULL REFERENCES users(id), + revoker_id INTEGER REFERENCES users(id), + active BOOLEAN DEFAULT 'true' CHECK (active), + CONSTRAINT active_revoke_sane CHECK ( + (active IS NULL AND revoke_event IS NOT NULL AND revoker_id IS NOT NULL) + OR (active IS NOT NULL AND revoke_event IS NULL AND revoker_id IS NULL)), + PRIMARY KEY (create_event, host_id), + UNIQUE (host_id,active) +) WITHOUT OIDS; + +-- copy starting data +-- CREATE FUNCTION pg_temp.user() returns INTEGER as $$ select id from users where name='nobody' $$ language SQL; +CREATE FUNCTION pg_temp.user() returns INTEGER as $$ select 1 $$ language SQL; +-- If you would like to use an existing user instead, then: +-- 1. edit the temporary function to look for the alternate user name + +SELECT 'Copying data from host to host_config'; +INSERT INTO host_config (host_id, arches, capacity, description, comment, enabled, creator_id) + SELECT id, arches, capacity, description, comment, enabled, pg_temp.user() FROM host; + +-- alter original table +SELECT 'Dropping moved columns'; +ALTER TABLE host DROP COLUMN arches; +ALTER TABLE host DROP COLUMN capacity; +ALTER TABLE host DROP COLUMN description; +ALTER TABLE host DROP COLUMN comment; +ALTER TABLE host DROP COLUMN enabled; + +COMMIT; diff --git a/hub/kojihub.py b/hub/kojihub.py index a21df6d0..6e958fec 100644 --- a/hub/kojihub.py +++ b/hub/kojihub.py @@ -2030,11 +2030,18 @@ def readTagGroups(tag, event=None, inherit=True, incl_pkgs=True, incl_reqs=True) def set_host_enabled(hostname, enabled=True): context.session.assertPerm('admin') - if not get_host(hostname): + host = get_host(hostname) + if not host: raise koji.GenericError('host does not exist: %s' % hostname) - c = context.cnx.cursor() - c.execute("""UPDATE host SET enabled = %(enabled)s WHERE name = %(hostname)s""", locals()) - context.commit_pending = True + + update = UpdateProcessor('host_config', values=host, clauses=['host_id = %(id)i']) + update.make_revoke() + update.execute() + + insert = InsertProcessor('host_config', data=dslice(host, ('user_id', 'name', 'arches', 'capacity', 'description', 'comment', 'enabled'))) + insert.set(host_id=host['id'], enabled=enabled) + insert.make_create() + insert.execute() def add_host_to_channel(hostname, channel_name, create=False): """Add the host to the specified channel @@ -4504,7 +4511,7 @@ def _dml(operation, values): context.commit_pending = True return ret -def get_host(hostInfo, strict=False): +def get_host(hostInfo, strict=False, event=None): """Get information about the given host. hostInfo may be either a string (hostname) or int (host id). A map will be returned containing the following data: @@ -4520,18 +4527,39 @@ def get_host(hostInfo, strict=False): - ready - enabled """ - fields = ('id', 'user_id', 'name', 'arches', 'task_load', - 'capacity', 'description', 'comment', 'ready', 'enabled') - query = """SELECT %s FROM host - WHERE """ % ', '.join(fields) + tables = ['host_config'] + joins = ['host ON host.id = host_config.host_id'] + + fields = {'host.id': 'id', + 'host.user_id': 'user_id', + 'host.name': 'name', + 'host.ready': 'ready', + 'host.task_load': 'task_load', + 'host_config.arches': 'arches', + 'host_config.capacity': 'capacity', + 'host_config.description': 'description', + 'host_config.comment': 'comment', + 'host_config.enabled': 'enabled', + } + clauses = [eventCondition(event, table='host_config')] + if isinstance(hostInfo, int) or isinstance(hostInfo, long): - query += """id = %(hostInfo)i""" + clauses.append("id = %(hostInfo)i") elif isinstance(hostInfo, str): - query += """name = %(hostInfo)s""" + clauses.append("name = %(hostInfo)s") else: raise koji.GenericError('invalid type for hostInfo: %s' % type(hostInfo)) - return _singleRow(query, locals(), fields, strict) + data = {'hostInfo': hostInfo} + fields, aliases = zip(*fields.items()) + query = QueryProcessor(columns=fields, aliases=aliases, tables=tables, + joins=joins, clauses=clauses, values=data) + result = query.executeOne() + if not result: + if strict: + raise koji.GenericError('Invalid hostInfo: %s' % hostInfo) + return None + return result def edit_host(hostInfo, **kw): """Edit information for an existing host. @@ -4553,19 +4581,22 @@ def edit_host(hostInfo, **kw): changes = [] for field in fields: if field in kw and kw[field] != host[field]: - if field == 'capacity': - # capacity is a float, so set the substitution format appropriately - changes.append('%s = %%(%s)f' % (field, field)) - else: - changes.append('%s = %%(%s)s' % (field, field)) + changes.append(field) if not changes: return False - update = 'UPDATE host set ' + ', '.join(changes) + ' where id = %(id)i' - data = kw.copy() - data['id'] = host['id'] - _dml(update, data) + update = UpdateProcessor('host_config', values=host, clauses=['host_id = %(id)i']) + update.make_revoke() + update.execute() + + insert = InsertProcessor('host_config', data=dslice(host, ('arches', 'capacity', 'description', 'comment', 'enabled'))) + insert.set(host_id=host['id']) + for change in changes: + insert.set(**{change: kw[change]}) + insert.make_create() + insert.execute() + return True def get_channel(channelInfo, strict=False): @@ -6556,6 +6587,7 @@ def query_history(tables=None, **kwargs): 'tag_extra': ['tag_id', 'key', 'value'], 'build_target_config': ['build_target_id', 'build_tag', 'dest_tag'], 'external_repo_config': ['external_repo_id', 'url'], + 'host_config': ['host_id', 'arches', 'capacity', 'description', 'comment', 'enabled'], 'tag_external_repos': ['tag_id', 'external_repo_id', 'priority'], 'tag_listing': ['build_id', 'tag_id'], 'tag_packages': ['package_id', 'tag_id', 'owner', 'blocked', 'extra_arches'], @@ -6572,6 +6604,7 @@ def query_history(tables=None, **kwargs): 'cg_id': ['content_generator'], #group_id is overloaded (special case below) 'tag_id': ['tag'], + 'host_id': ['host'], 'parent_id': ['tag', 'parent'], 'build_target_id': ['build_target'], 'build_tag': ['tag', 'build_tag'], @@ -10577,10 +10610,14 @@ class RootExports(object): krb_principal=krb_principal) #host entry hostID = _singleValue("SELECT nextval('host_id_seq')", strict=True) - arches = " ".join(arches) - insert = """INSERT INTO host (id, user_id, name, arches) - VALUES (%(hostID)i, %(userID)i, %(hostname)s, %(arches)s)""" + insert = "INSERT INTO host (id, user_id, name) VALUES (%(hostID)i, %(userID)i, %(hostname)s" _dml(insert, locals()) + + insert = InsertProcessor('host_config') + insert.set(host_id=hostID, arches=" ".join(arches)) + insert.make_create() + insert.execute() + #host_channels entry insert = """INSERT INTO host_channels (host_id, channel_id) VALUES (%(hostID)i, %(default_channel)i)""" @@ -10608,11 +10645,8 @@ class RootExports(object): host appears in the list, it will be included in the results. If "ready" and "enabled" are specified, only hosts with the given value for the respective field will be included.""" - fields = ('id', 'user_id', 'name', 'arches', 'task_load', - 'capacity', 'description', 'comment', 'ready', 'enabled') - - clauses = [] - joins = [] + clauses = ['active IS TRUE'] + joins = ['host ON host.id = host_config.host_id'] if arches is not None: if not arches: raise koji.GenericError('arches option cannot be empty') @@ -10624,25 +10658,37 @@ class RootExports(object): clauses.append('(' + ' OR '.join(archClause) + ')') if channelID is not None: channelID = get_channel_id(channelID, strict=True) - joins.append('host_channels on host.id = host_channels.host_id') + joins.append('host_channels ON host.id = host_channels.host_id') clauses.append('host_channels.channel_id = %(channelID)i') if ready is not None: if ready: - clauses.append('ready is true') + clauses.append('ready IS TRUE') else: - clauses.append('ready is false') + clauses.append('ready IS FALSE') if enabled is not None: if enabled: - clauses.append('enabled is true') + clauses.append('enabled IS TRUE') else: - clauses.append('enabled is false') + clauses.append('enabled IS FALSE') if userID is not None: userID = get_user(userID, strict=True)['id'] clauses.append('user_id = %(userID)i') - query = QueryProcessor(columns=fields, tables=['host'], - joins=joins, clauses=clauses, - values=locals(), opts=queryOpts) + fields = {'host.id': 'id', + 'host.user_id': 'user_id', + 'host.name': 'name', + 'host.ready': 'ready', + 'host.task_load': 'task_load', + 'host_config.arches': 'arches', + 'host_config.capacity': 'capacity', + 'host_config.description': 'description', + 'host_config.comment': 'comment', + 'host_config.enabled': 'enabled', + } + tables = ['host_config'] + fields, aliases = zip(*fields.items()) + query = QueryProcessor(columns=fields, aliases=aliases, + tables=tables, joins=joins, clauses=clauses, values=locals()) return query.execute() def getLastHostUpdate(self, hostID): diff --git a/tests/test_hub/test_list_hosts.py b/tests/test_hub/test_list_hosts.py index 62f2002b..ba80fcd6 100644 --- a/tests/test_hub/test_list_hosts.py +++ b/tests/test_hub/test_list_hosts.py @@ -29,9 +29,9 @@ class TestListHosts(unittest.TestCase): self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, []) - self.assertEqual(query.clauses, []) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE',]) @mock.patch('kojihub.get_user') def test_list_hosts_user_id(self, get_user): @@ -40,9 +40,9 @@ class TestListHosts(unittest.TestCase): self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, []) - self.assertEqual(query.clauses, ['user_id = %(userID)i']) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE', 'user_id = %(userID)i']) @mock.patch('kojihub.get_channel_id') def test_list_hosts_channel_id(self, get_channel_id): @@ -51,27 +51,28 @@ class TestListHosts(unittest.TestCase): self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, ['host_channels on host.id = host_channels.host_id']) - self.assertEqual(query.clauses, ['host_channels.channel_id = %(channelID)i']) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id', + 'host_channels ON host.id = host_channels.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE','host_channels.channel_id = %(channelID)i']) def test_list_hosts_single_arch(self): self.exports.listHosts(arches='x86_64') self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, []) - self.assertEqual(query.clauses, [r"""(arches ~ E'\\mx86_64\\M')"""]) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE',r"""(arches ~ E'\\mx86_64\\M')"""]) def test_list_hosts_multi_arch(self): self.exports.listHosts(arches=['x86_64', 's390']) self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, []) - self.assertEqual(query.clauses, [r"""(arches ~ E'\\mx86_64\\M' OR arches ~ E'\\ms390\\M')"""]) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE',r"""(arches ~ E'\\mx86_64\\M' OR arches ~ E'\\ms390\\M')"""]) def test_list_hosts_bad_arch(self): with self.assertRaises(koji.GenericError): @@ -82,33 +83,33 @@ class TestListHosts(unittest.TestCase): self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, []) - self.assertEqual(query.clauses, ['ready is true']) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE','ready IS TRUE']) def test_list_hosts_nonready(self): self.exports.listHosts(ready=0) self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, []) - self.assertEqual(query.clauses, ['ready is false']) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE','ready IS FALSE']) def test_list_hosts_enabled(self): self.exports.listHosts(enabled=1) self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, []) - self.assertEqual(query.clauses, ['enabled is true']) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE','enabled IS TRUE']) def test_list_hosts_disabled(self): self.exports.listHosts(enabled=0) self.assertEqual(len(self.queries), 1) query = self.queries[0] - self.assertEqual(query.tables, ['host']) - self.assertEqual(query.joins, []) - self.assertEqual(query.clauses, ['enabled is false']) + self.assertEqual(query.tables, ['host_config']) + self.assertEqual(query.joins, ['host ON host.id = host_config.host_id']) + self.assertEqual(query.clauses, ['active IS TRUE','enabled IS FALSE'])