Skip to content

Commit 4ca42f9

Browse files
fraugabelmbuechse
andauthored
Rework default-security-group-rules.py (#748)
This makes the test for the default rules of security groups downwards compatible with versions of OpenStack that don't have network.default_security_group_rules(). Solves #746. Signed-off-by: Katharina Trentau <[email protected]> Signed-off-by: Matthias Büchse <[email protected]> Co-authored-by: Matthias Büchse <[email protected]>
1 parent bf0c077 commit 4ca42f9

File tree

2 files changed

+146
-95
lines changed

2 files changed

+146
-95
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
**/__pycache__/
22
.venv/
33
.idea
4+
.sandbox
45
.DS_Store
56
node_modules
67
Tests/kaas/results/

Tests/iaas/security-groups/default-security-group-rules.py

100644100755
Lines changed: 145 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -4,127 +4,177 @@
44
except for ingress rules from the same Security Group. Furthermore the
55
presence of default rules for egress traffic is checked.
66
"""
7+
import argparse
8+
from collections import Counter
9+
import logging
10+
import os
11+
import sys
712

813
import openstack
9-
import os
10-
import argparse
14+
from openstack.exceptions import ResourceNotFound
1115

16+
logger = logging.getLogger(__name__)
1217

13-
def connect(cloud_name: str) -> openstack.connection.Connection:
14-
"""Create a connection to an OpenStack cloud
18+
SG_NAME = "scs-test-default-sg"
19+
DESCRIPTION = "scs-test-default-sg"
1520

16-
:param string cloud_name:
17-
The name of the configuration to load from clouds.yaml.
1821

19-
:returns: openstack.connnection.Connection
22+
def check_default_rules(rules, short=False):
2023
"""
21-
return openstack.connect(
22-
cloud=cloud_name,
23-
)
24+
counts all verall ingress rules and egress rules, depending on the requested testing mode
2425
25-
26-
def test_rules(cloud_name: str):
27-
try:
28-
connection = connect(cloud_name)
29-
rules = connection.network.default_security_group_rules()
30-
except Exception as e:
31-
print(str(e))
32-
raise Exception(
33-
f"Connection to cloud '{cloud_name}' was not successfully. "
34-
f"The default Security Group Rules could not be accessed. "
35-
f"Please check your cloud connection and authorization."
36-
)
37-
38-
# count all overall ingress rules and egress rules.
39-
ingress_rules = 0
40-
ingress_from_same_sg = 0
41-
egress_rules = 0
42-
egress_ipv4_default_sg = 0
43-
egress_ipv4_custom_sg = 0
44-
egress_ipv6_default_sg = 0
45-
egress_ipv6_custom_sg = 0
26+
:param bool short
27+
if short is True, the testing mode is set on short for older OpenStack versions
28+
"""
29+
ingress_rules = egress_rules = 0
30+
egress_vars = {'IPv4': {}, 'IPv6': {}}
31+
for key, value in egress_vars.items():
32+
value['default'] = 0
33+
if not short:
34+
value['custom'] = 0
4635
if not rules:
47-
print("No default security group rules defined.")
48-
else:
49-
for rule in rules:
50-
direction = rule.direction
51-
ethertype = rule.ethertype
52-
r_custom_sg = rule.used_in_non_default_sg
53-
r_default_sg = rule.used_in_default_sg
54-
if direction == "ingress":
55-
ingress_rules += 1
36+
logger.info("No default security group rules defined.")
37+
for rule in rules:
38+
direction = rule["direction"]
39+
ethertype = rule["ethertype"]
40+
if direction == "ingress":
41+
if not short:
5642
# we allow ingress from the same security group
5743
# but only for the default security group
58-
r_group_id = rule.remote_group_id
59-
if (r_group_id == "PARENT" and not r_custom_sg):
60-
ingress_from_same_sg += 1
61-
elif direction == "egress" and ethertype == "IPv4":
62-
egress_rules += 1
63-
if rule.remote_ip_prefix:
64-
# this rule does not allow traffic to all external ips
65-
continue
66-
if r_custom_sg:
67-
egress_ipv4_custom_sg += 1
68-
if r_default_sg:
69-
egress_ipv4_default_sg += 1
70-
elif direction == "egress" and ethertype == "IPv6":
71-
egress_rules += 1
72-
if rule.remote_ip_prefix:
73-
# this rule does not allow traffic to all external ips
44+
if rule.remote_group_id == "PARENT" and not rule["used_in_non_default_sg"]:
7445
continue
75-
if r_custom_sg:
76-
egress_ipv6_custom_sg += 1
77-
if r_default_sg:
78-
egress_ipv6_default_sg += 1
79-
80-
# test whether there are no other than the allowed ingress rules
81-
assert ingress_rules == ingress_from_same_sg, (
82-
f"Expected only ingress rules for default security groups, "
83-
f"that allow ingress traffic from the same group. "
84-
f"But there are more - in total {ingress_rules} ingress rules. "
85-
f"There should be only {ingress_from_same_sg} ingress rules.")
86-
assert egress_rules > 0, (
87-
f"Expected to have more than {egress_rules} egress rules present.")
88-
var_list = [egress_ipv4_default_sg, egress_ipv4_custom_sg,
89-
egress_ipv6_default_sg, egress_ipv6_custom_sg]
90-
assert all([var > 0 for var in var_list]), (
91-
"Not all expected egress rules are present. "
92-
"Expected rules for egress for IPv4 and IPv6 "
93-
"both for default and custom security groups.")
94-
95-
result_dict = {
96-
"Ingress Rules": ingress_rules,
97-
"Egress Rules": egress_rules
98-
}
99-
return result_dict
46+
ingress_rules += 1
47+
elif direction == "egress" and ethertype in egress_vars:
48+
egress_rules += 1
49+
if short:
50+
egress_vars[ethertype]['default'] += 1
51+
continue
52+
if rule.remote_ip_prefix:
53+
# this rule does not allow traffic to all external ips
54+
continue
55+
# note: these two are not mutually exclusive
56+
if rule["used_in_default_sg"]:
57+
egress_vars[ethertype]['default'] += 1
58+
if rule["used_in_non_default_sg"]:
59+
egress_vars[ethertype]['custom'] += 1
60+
# test whether there are no unallowed ingress rules
61+
if ingress_rules:
62+
logger.error(f"Expected no default ingress rules, found {ingress_rules}.")
63+
# test whether all expected egress rules are present
64+
missing = [(key, key2) for key, val in egress_vars.items() for key2, val2 in val.items() if not val2]
65+
if missing:
66+
logger.error(
67+
"Expected rules for egress for IPv4 and IPv6 both for default and custom security groups. "
68+
f"Missing rule types: {', '.join(str(x) for x in missing)}"
69+
)
70+
logger.info(str({
71+
"Unallowed Ingress Rules": ingress_rules,
72+
"Egress Rules": egress_rules,
73+
}))
74+
75+
76+
def create_security_group(conn, sg_name: str = SG_NAME, description: str = DESCRIPTION):
77+
"""Create security group in openstack
78+
79+
:returns:
80+
~openstack.network.v2.security_group.SecurityGroup: The new security group or None
81+
"""
82+
sg = conn.network.create_security_group(name=sg_name, description=description)
83+
return sg.id
84+
85+
86+
def delete_security_group(conn, sg_id):
87+
conn.network.delete_security_group(sg_id)
88+
# in case of a successful delete finding the sg will throw an exception
89+
try:
90+
conn.network.find_security_group(name_or_id=sg_id)
91+
except ResourceNotFound:
92+
logger.debug(f"Security group {sg_id} was deleted successfully.")
93+
except Exception:
94+
logger.critical(f"Security group {sg_id} was not deleted successfully")
95+
raise
96+
97+
98+
def altern_test_rules(connection: openstack.connection.Connection):
99+
sg_id = create_security_group(connection)
100+
try:
101+
sg = connection.network.find_security_group(name_or_id=sg_id)
102+
check_default_rules(sg.security_group_rules, short=True)
103+
finally:
104+
delete_security_group(connection, sg_id)
105+
106+
107+
def test_rules(connection: openstack.connection.Connection):
108+
try:
109+
rules = list(connection.network.default_security_group_rules())
110+
except ResourceNotFound:
111+
logger.info(
112+
"API call failed. OpenStack components might not be up to date. "
113+
"Falling back to old-style test method. "
114+
)
115+
logger.debug("traceback", exc_info=True)
116+
altern_test_rules(connection)
117+
else:
118+
check_default_rules(rules)
119+
120+
121+
class CountingHandler(logging.Handler):
122+
def __init__(self, level=logging.NOTSET):
123+
super().__init__(level=level)
124+
self.bylevel = Counter()
125+
126+
def handle(self, record):
127+
self.bylevel[record.levelno] += 1
100128

101129

102130
def main():
103131
parser = argparse.ArgumentParser(
104-
description="SCS Default Security Group Rules Checker")
132+
description="SCS Default Security Group Rules Checker",
133+
)
105134
parser.add_argument(
106-
"--os-cloud", type=str,
135+
"--os-cloud",
136+
type=str,
107137
help="Name of the cloud from clouds.yaml, alternative "
108-
"to the OS_CLOUD environment variable"
138+
"to the OS_CLOUD environment variable",
109139
)
110140
parser.add_argument(
111-
"--debug", action="store_true",
112-
help="Enable OpenStack SDK debug logging"
141+
"--debug", action="store_true", help="Enable debug logging",
113142
)
114143
args = parser.parse_args()
115144
openstack.enable_logging(debug=args.debug)
145+
logging.basicConfig(
146+
format="%(levelname)s: %(message)s",
147+
level=logging.DEBUG if args.debug else logging.INFO,
148+
)
149+
150+
# count the number of log records per level (used for summary and return code)
151+
counting_handler = CountingHandler(level=logging.INFO)
152+
logger.addHandler(counting_handler)
116153

117154
# parse cloud name for lookup in clouds.yaml
118-
cloud = os.environ.get("OS_CLOUD", None)
119-
if args.os_cloud:
120-
cloud = args.os_cloud
121-
assert cloud, (
122-
"You need to have the OS_CLOUD environment variable set to your cloud "
123-
"name or pass it via --os-cloud"
124-
)
155+
cloud = args.os_cloud or os.environ.get("OS_CLOUD", None)
156+
if not cloud:
157+
raise ValueError(
158+
"You need to have the OS_CLOUD environment variable set to your cloud "
159+
"name or pass it via --os-cloud"
160+
)
125161

126-
print(test_rules(cloud))
162+
with openstack.connect(cloud) as conn:
163+
test_rules(conn)
164+
165+
c = counting_handler.bylevel
166+
logger.debug(f"Total critical / error / warning: {c[logging.CRITICAL]} / {c[logging.ERROR]} / {c[logging.WARNING]}")
167+
if not c[logging.CRITICAL]:
168+
print("security-groups-default-rules-check: " + ('PASS', 'FAIL')[min(1, c[logging.ERROR])])
169+
return min(127, c[logging.CRITICAL] + c[logging.ERROR]) # cap at 127 due to OS restrictions
127170

128171

129172
if __name__ == "__main__":
130-
main()
173+
try:
174+
sys.exit(main())
175+
except SystemExit:
176+
raise
177+
except BaseException as exc:
178+
logging.debug("traceback", exc_info=True)
179+
logging.critical(str(exc))
180+
sys.exit(1)

0 commit comments

Comments
 (0)