Skip to content

Commit b040175

Browse files
authored
Add outage tracking (#325)
* move migrations * start on outages * stuff * initial outage interface * fix spec syntax * system outage testing * report outages and api testing * remove commented out table * clarify * goto 60 in docker-compose for tests * add outages key to report fixture * cast outages to list * more testing * fix delete outage test, storage interface list reports test * remove unneeded try block * lint, ugh * load system outages when exclude_system_outages selected * linting * fix test * move migration back to 57 * fix docker compose * mysql tests partially * fix sql test * remove restrictions on outage time range except start > report_end
1 parent b3512d5 commit b040175

File tree

16 files changed

+1153
-11
lines changed

16 files changed

+1153
-11
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
DROP TABLE arbiter_data.system_outages;
2+
DROP TABLE arbiter_data.report_outages;
3+
DROP PROCEDURE store_system_outage;
4+
DROP PROCEDURE list_system_outages;
5+
DROP PROCEDURE read_system_outage;
6+
DROP PROCEDURE delete_system_outage;
7+
DROP PROCEDURE store_report_outage;
8+
DROP PROCEDURE list_report_outages;
9+
DROP PROCEDURE delete_report_outage;
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
-- Table for storing system outage information with start, end information
2+
CREATE TABLE arbiter_data.system_outages(
3+
id BINARY(16) NOT NULL DEFAULT(UUID_TO_BIN(UUID(), 1)),
4+
start TIMESTAMP NOT NULL,
5+
end TIMESTAMP NOT NULL,
6+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
7+
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
8+
9+
PRIMARY KEY(id)
10+
) ENGINE=INNODB ENCRYPTION='Y' ROW_FORMAT=COMPRESSED;
11+
12+
GRANT SELECT ON arbiter_data.system_outages TO 'select_objects'@'localhost';
13+
GRANT INSERT ON arbiter_data.system_outages TO 'insert_objects'@'localhost';
14+
GRANT SELECT, DELETE ON arbiter_data.system_outages TO 'delete_objects'@'localhost';
15+
16+
17+
-- Table for storing report-specific outage with start, end information
18+
CREATE TABLE arbiter_data.report_outages(
19+
id BINARY(16) NOT NULL DEFAULT(UUID_TO_BIN(UUID(), 1)),
20+
report_id BINARY(16) NOT NULL,
21+
start TIMESTAMP NOT NULL,
22+
end TIMESTAMP NOT NULL,
23+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
24+
modified_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
25+
26+
PRIMARY KEY(id),
27+
FOREIGN KEY (report_id)
28+
REFERENCES reports(id)
29+
ON DELETE CASCADE
30+
) ENGINE=INNODB ENCRYPTION='Y' ROW_FORMAT=COMPRESSED;
31+
32+
GRANT SELECT ON arbiter_data.report_outages TO 'select_objects'@'localhost';
33+
GRANT INSERT ON arbiter_data.report_outages TO 'insert_objects'@'localhost';
34+
GRANT SELECT, DELETE ON arbiter_data.report_outages TO 'delete_objects'@'localhost';
35+
36+
37+
-- System outage procedures
38+
CREATE DEFINER = 'insert_objects'@'localhost' PROCEDURE store_system_outage (
39+
IN strid VARCHAR(36), IN start TIMESTAMP, IN end TIMESTAMP)
40+
MODIFIES SQL DATA SQL SECURITY DEFINER
41+
BEGIN
42+
DECLARE binid BINARY(16);
43+
SET binid = (SELECT UUID_TO_BIN(strid, 1));
44+
INSERT INTO arbiter_data.system_outages(id, start, end) VALUES (
45+
binid, start, end
46+
);
47+
END;
48+
49+
GRANT EXECUTE ON PROCEDURE arbiter_data.store_system_outage TO 'insert_objects'@'localhost';
50+
GRANT EXECUTE ON PROCEDURE arbiter_data.store_system_outage TO 'frameworkadmin'@'%';
51+
52+
CREATE DEFINER = 'select_objects'@'localhost' PROCEDURE list_system_outages ()
53+
READS SQL DATA SQL SECURITY DEFINER
54+
BEGIN
55+
SELECT BIN_TO_UUID(id, 1) as outage_id, start, end, created_at, modified_at
56+
FROM arbiter_data.system_outages;
57+
END;
58+
59+
GRANT EXECUTE ON PROCEDURE arbiter_data.list_system_outages TO 'select_objects'@'localhost';
60+
GRANT EXECUTE ON PROCEDURE arbiter_data.list_system_outages TO 'frameworkadmin'@'%';
61+
-- apiuser should only be able to list system outages, not store or delete them
62+
GRANT EXECUTE ON PROCEDURE arbiter_data.list_system_outages TO 'apiuser'@'%';
63+
64+
CREATE DEFINER = 'select_objects'@'localhost' PROCEDURE read_system_outage(
65+
In strid VARCHAR(36))
66+
READS SQL DATA SQL SECURITY DEFINER
67+
BEGIN
68+
DECLARE binid BINARY(16);
69+
SET binid = (SELECT UUID_TO_BIN(strid, 1));
70+
SELECT BIN_TO_UUID(id, 1) as outage_id, start, end, created_at, modified_at
71+
FROM arbiter_data.system_outages
72+
WHERE id = binid;
73+
END;
74+
75+
GRANT EXECUTE ON PROCEDURE arbiter_data.read_system_outage TO 'select_objects'@'localhost';
76+
GRANT EXECUTE ON PROCEDURE arbiter_data.read_system_outage TO 'frameworkadmin'@'%';
77+
78+
CREATE DEFINER = 'delete_objects'@'localhost' PROCEDURE delete_system_outage(
79+
In strid VARCHAR(36))
80+
MODIFIES SQL DATA SQL SECURITY DEFINER
81+
BEGIN
82+
DECLARE binid BINARY(16);
83+
SET binid = (SELECT UUID_TO_BIN(strid, 1));
84+
DELETE FROM arbiter_data.system_outages WHERE id = binid;
85+
END;
86+
87+
88+
GRANT EXECUTE ON PROCEDURE arbiter_data.delete_system_outage TO 'delete_objects'@'localhost';
89+
GRANT EXECUTE ON PROCEDURE arbiter_data.delete_system_outage TO 'frameworkadmin'@'%';
90+
91+
92+
-- Report outage procedure
93+
CREATE DEFINER = 'insert_objects'@'localhost' PROCEDURE store_report_outage (
94+
IN auth0id VARCHAR(32), IN str_reportid VARCHAR(36),
95+
IN str_outageid VARCHAR(36), IN start TIMESTAMP, IN end TIMESTAMP
96+
)
97+
MODIFIES SQL DATA SQL SECURITY DEFINER
98+
BEGIN
99+
DECLARE bin_reportid BINARY(16);
100+
DECLARE bin_outageid BINARY(16);
101+
DECLARE allowed BOOLEAN DEFAULT FALSE;
102+
103+
SET bin_reportid = (SELECT UUID_TO_BIN(str_reportid, 1));
104+
SET bin_outageid = (SELECT UUID_TO_BIN(str_outageid, 1));
105+
106+
-- Check that the user has update permissions
107+
SET allowed = (SELECT can_user_perform_action(auth0id, bin_reportid, 'update'));
108+
IF allowed THEN
109+
INSERT INTO arbiter_data.report_outages(
110+
id, report_id, start, end
111+
) VALUES (
112+
bin_outageid, bin_reportid, start, end
113+
);
114+
ELSE
115+
SIGNAL SQLSTATE '42000' SET MESSAGE_TEXT = 'Access denied to user on "update report for report outage write"',
116+
MYSQL_ERRNO = 1142;
117+
END IF;
118+
END;
119+
120+
GRANT EXECUTE ON PROCEDURE arbiter_data.store_report_outage TO 'insert_objects'@'localhost';
121+
GRANT EXECUTE ON PROCEDURE arbiter_data.store_report_outage TO 'apiuser'@'%';
122+
123+
CREATE DEFINER = 'select_objects'@'localhost' PROCEDURE list_report_outages (
124+
IN auth0id VARCHAR(32), IN str_reportid VARCHAR(36)
125+
)
126+
READS SQL DATA SQL SECURITY DEFINER
127+
BEGIN
128+
DECLARE bin_reportid BINARY(16);
129+
DECLARE allowed BOOLEAN DEFAULT FALSE;
130+
131+
SET bin_reportid = (SELECT UUID_TO_BIN(str_reportid, 1));
132+
SET allowed = (SELECT can_user_perform_action(auth0id, bin_reportid, 'read'));
133+
134+
IF allowed THEN
135+
SELECT BIN_TO_UUID(id, 1) as outage_id, BIN_TO_UUID(report_id, 1) as report_id, start, end, created_at, modified_at
136+
FROM arbiter_data.report_outages
137+
WHERE report_id = bin_reportid;
138+
ELSE
139+
SIGNAL SQLSTATE '42000' SET MESSAGE_TEXT = 'Access denied to user on "read report outages"',
140+
MYSQL_ERRNO = 1142;
141+
END IF;
142+
143+
END;
144+
145+
GRANT EXECUTE ON PROCEDURE arbiter_data.list_report_outages TO 'select_objects'@'localhost';
146+
GRANT EXECUTE ON PROCEDURE arbiter_data.list_report_outages TO 'apiuser'@'%';
147+
148+
CREATE DEFINER = 'delete_objects'@'localhost' PROCEDURE delete_report_outage(
149+
IN auth0id VARCHAR(32), IN str_reportid VARCHAR(36), IN str_outageid VARCHAR(36)
150+
)
151+
MODIFIES SQL DATA SQL SECURITY DEFINER
152+
BEGIN
153+
DECLARE bin_reportid BINARY(16);
154+
DECLARE bin_outageid BINARY(16);
155+
DECLARE allowed BOOLEAN DEFAULT FALSE;
156+
157+
SET bin_reportid = (SELECT UUID_TO_BIN(str_reportid, 1));
158+
SET bin_outageid = (SELECT UUID_TO_BIN(str_outageid, 1));
159+
SET allowed = (SELECT can_user_perform_action(auth0id, bin_reportid, 'update'));
160+
161+
IF allowed THEN
162+
DELETE FROM arbiter_data.report_outages WHERE id = bin_outageid;
163+
ELSE
164+
SIGNAL SQLSTATE '42000' SET MESSAGE_TEXT = 'Access denied to user on "update report for report outage delete"',
165+
MYSQL_ERRNO = 1142;
166+
END IF;
167+
END;
168+
169+
170+
GRANT EXECUTE ON PROCEDURE arbiter_data.delete_report_outage TO 'delete_objects'@'localhost';
171+
GRANT EXECUTE ON PROCEDURE arbiter_data.delete_report_outage TO 'apiuser'@'%';

datastore/tests/test_outages.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import datetime
2+
from conftest import newuuid, bin_to_uuid
3+
4+
5+
import pymysql
6+
import pytest
7+
8+
9+
@pytest.fixture()
10+
def add_perm(cursor, new_permission, valueset):
11+
# Add a permission to user0, role0 of valueset
12+
role_id = valueset[2][0]['id']
13+
org = valueset[0][0]
14+
15+
def fcn(action, what):
16+
perm = new_permission(action, what, True, org=org)
17+
cursor.execute(
18+
'INSERT INTO role_permission_mapping (role_id, permission_id) '
19+
'VALUES (%s, %s)', (role_id, perm['id'])
20+
)
21+
return fcn
22+
23+
24+
@pytest.fixture
25+
def one_system_outage(cursor):
26+
some_uuid = bin_to_uuid(newuuid())
27+
start = datetime.datetime(2021, 4, 14, 12, 0)
28+
end = datetime.datetime(2021, 4, 14, 13, 0)
29+
30+
cursor.execute(
31+
'CALL store_system_outage(%s, %s, %s)',
32+
(some_uuid,
33+
start.strftime('%Y-%m-%d %H:%M'),
34+
end.strftime('%Y-%m-%d %H:%M')
35+
)
36+
)
37+
return some_uuid
38+
39+
40+
def test_get_system_outages(cursor):
41+
cursor.execute('CALL list_system_outages()')
42+
assert cursor.fetchall() == ()
43+
44+
45+
def test_add_remove_system_otuage(cursor):
46+
some_uuid = bin_to_uuid(newuuid())
47+
start = datetime.datetime(2021, 4, 14, 12, 0)
48+
end = datetime.datetime(2021, 4, 14, 13, 0)
49+
50+
cursor.execute(
51+
'CALL store_system_outage(%s, %s, %s)',
52+
(some_uuid,
53+
start.strftime('%Y-%m-%d %H:%M'),
54+
end.strftime('%Y-%m-%d %H:%M')
55+
)
56+
)
57+
cursor.execute('CALL list_system_outages()')
58+
outages = cursor.fetchall()
59+
assert len(outages) == 1
60+
outage = outages[0]
61+
62+
assert outage[0] == some_uuid
63+
assert outage[1] == start
64+
assert outage[2] == end
65+
cursor.execute('CALL delete_system_outage(%s)', some_uuid)
66+
67+
cursor.execute('CALL list_system_outages()')
68+
outages = cursor.fetchall()
69+
assert len(outages) == 0
70+
71+
72+
def test_report_outage_no_perms(cursor, valueset, new_report):
73+
org = valueset[0][0]
74+
user = valueset[1]
75+
auth0_id = user[0]['auth0_id']
76+
report = new_report(org=org)
77+
report_id = bin_to_uuid(report['id'])
78+
some_uuid = bin_to_uuid(newuuid())
79+
start = datetime.datetime(2021, 4, 14, 12, 0)
80+
end = datetime.datetime(2021, 4, 14, 13, 0)
81+
with pytest.raises(pymysql.err.OperationalError):
82+
cursor.execute(
83+
"CALL store_report_outage(%s, %s, %s, %s, %s)",
84+
(auth0_id, report_id, some_uuid,
85+
start.strftime('%Y-%m-%d %H:%M'),
86+
end.strftime('%Y-%m-%d %H:%M'))
87+
)
88+
89+
90+
def test_store_and_read_report_outage(cursor, valueset, new_report, add_perm):
91+
org = valueset[0][0]
92+
user = valueset[1]
93+
auth0_id = user[0]['auth0_id']
94+
add_perm("read", "reports")
95+
add_perm("update", "reports")
96+
report = new_report(org=org)
97+
report_id = bin_to_uuid(report['id'])
98+
some_uuid = bin_to_uuid(newuuid())
99+
start = datetime.datetime(2021, 4, 14, 12, 0)
100+
end = datetime.datetime(2021, 4, 14, 13, 0)
101+
cursor.execute(
102+
"CALL store_report_outage(%s, %s, %s, %s, %s)",
103+
(auth0_id, report_id, some_uuid,
104+
start.strftime('%Y-%m-%d %H:%M'),
105+
end.strftime('%Y-%m-%d %H:%M'))
106+
)
107+
cursor.execute(
108+
"CALL list_report_outages(%s, %s)",
109+
(auth0_id, report_id)
110+
)
111+
outages = cursor.fetchall()
112+
assert len(outages) == 1
113+
outage = outages[0]
114+
assert outage[0] == some_uuid
115+
assert outage[1] == report_id
116+
assert outage[2] == start
117+
assert outage[3] == end
118+
119+
120+
def test_delete_report_outage(cursor, valueset, new_report, add_perm):
121+
org = valueset[0][0]
122+
user = valueset[1]
123+
auth0_id = user[0]['auth0_id']
124+
add_perm("read", "reports")
125+
add_perm("update", "reports")
126+
report = new_report(org=org)
127+
report_id = report['id']
128+
some_uuid = newuuid()
129+
start = datetime.datetime(2021, 4, 14, 12, 0)
130+
end = datetime.datetime(2021, 4, 14, 13, 0)
131+
cursor.execute(
132+
"INSERT INTO report_outages (id, report_id, start, end) "
133+
"VALUES (%s, %s, %s, %s)",
134+
(some_uuid, report_id,
135+
start.strftime('%Y-%m-%d %H:%M'),
136+
end.strftime('%Y-%m-%d %H:%M'))
137+
)
138+
cursor.execute(
139+
"SELECT * FROM report_outages WHERE report_id = %s",
140+
(report_id,)
141+
)
142+
assert len(cursor.fetchall()) == 1
143+
cursor.execute(
144+
"CALL delete_report_outage(%s, %s, %s)",
145+
(auth0_id, bin_to_uuid(report_id), bin_to_uuid(some_uuid))
146+
)
147+
cursor.execute(
148+
"SELECT * FROM report_outages WHERE report_id = %s",
149+
(report_id,)
150+
)
151+
assert len(cursor.fetchall()) == 0

sfa_api/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ def create_app(config_name='ProductionConfig'):
6767
from sfa_api.reports import reports_blp
6868
from sfa_api.aggregates import agg_blp
6969
from sfa_api.zones import zone_blp
70+
from sfa_api.outages import outage_blp
7071

7172
for blp in (obs_blp, forecast_blp, site_blp, user_blp, user_email_blp,
72-
role_blp, permission_blp, reports_blp, agg_blp, zone_blp):
73+
role_blp, permission_blp, reports_blp, agg_blp, zone_blp,
74+
outage_blp):
7375
blp.before_request(protect_endpoint)
7476
app.register_blueprint(blp)
7577

0 commit comments

Comments
 (0)