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:
parent
7b559a0f02
commit
dfd097b440
4 changed files with 168 additions and 66 deletions
|
|
@ -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'],
|
||||
|
|
|
|||
47
docs/schema-upgrade-1.15-1.16.sql
Normal file
47
docs/schema-upgrade-1.15-1.16.sql
Normal 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;
|
||||
120
hub/kojihub.py
120
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):
|
||||
|
|
|
|||
|
|
@ -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'])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue