add history to edit_host

Hosts now have history.

host table was split to host (containing ephemereal and non-editable
data (load, activity, name, user_id)) and host_config containing
data changeable by admins (archs, capacity, ...). This table is
versioned and searchable via queryHistory.

Fixes: https://pagure.io/koji/issue/638
This commit is contained in:
Tomas Kopecek 2018-01-09 16:45:51 +01:00 committed by Mike McLean
parent 7b559a0f02
commit dfd097b440
4 changed files with 168 additions and 66 deletions

View file

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

View file

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

View file

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

View file

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