Kubernetes integration with OpenStack networking
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

network_policy.py 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  1. # Copyright 2018 Red Hat, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. from oslo_log import log as logging
  15. from neutronclient.common import exceptions as n_exc
  16. from kuryr_kubernetes import clients
  17. from kuryr_kubernetes import config
  18. from kuryr_kubernetes import constants
  19. from kuryr_kubernetes.controller.drivers import base
  20. from kuryr_kubernetes.controller.drivers import utils as driver_utils
  21. from kuryr_kubernetes import exceptions
  22. from kuryr_kubernetes import utils
  23. LOG = logging.getLogger(__name__)
  24. class NetworkPolicyDriver(base.NetworkPolicyDriver):
  25. """Provide security groups actions based on K8s Network Policies"""
  26. def __init__(self):
  27. self.neutron = clients.get_neutron_client()
  28. self.kubernetes = clients.get_kubernetes_client()
  29. def ensure_network_policy(self, policy, project_id):
  30. """Create security group rules out of network policies
  31. Triggered by events from network policies, this method ensures that
  32. security groups and security group rules are created or updated in
  33. reaction to kubernetes network policies events.
  34. In addition it returns the pods affected by the policy:
  35. - Creation: pods on the namespace of the created policy
  36. - Update: pods that needs to be updated in case of PodSelector
  37. modification, i.e., the pods that were affected by the previous
  38. PodSelector
  39. """
  40. LOG.debug("Creating network policy %s", policy['metadata']['name'])
  41. if self.get_kuryrnetpolicy_crd(policy):
  42. previous_selector = (
  43. self.update_security_group_rules_from_network_policy(policy))
  44. if previous_selector:
  45. return self.affected_pods(policy, previous_selector)
  46. if previous_selector is None:
  47. return self.namespaced_pods(policy)
  48. else:
  49. self.create_security_group_rules_from_network_policy(policy,
  50. project_id)
  51. def update_security_group_rules_from_network_policy(self, policy):
  52. """Update security group rules
  53. This method updates security group rules based on CRUD events gotten
  54. from a configuration or patch to an existing network policy
  55. """
  56. crd = self.get_kuryrnetpolicy_crd(policy)
  57. crd_name = crd['metadata']['name']
  58. LOG.debug("Already existing CRD %s", crd_name)
  59. sg_id = crd['spec']['securityGroupId']
  60. # Fetch existing SG rules from kuryrnetpolicy CRD
  61. existing_sg_rules = []
  62. existing_i_rules = crd['spec'].get('ingressSgRules')
  63. existing_e_rules = crd['spec'].get('egressSgRules')
  64. if existing_i_rules or existing_e_rules:
  65. existing_sg_rules = existing_i_rules + existing_e_rules
  66. existing_pod_selector = crd['spec'].get('podSelector')
  67. # Parse network policy update and get new ruleset
  68. i_rules, e_rules = self.parse_network_policy_rules(policy, sg_id)
  69. current_sg_rules = i_rules + e_rules
  70. # Get existing security group rules ids
  71. sgr_ids = [x['security_group_rule'].pop('id') for x in
  72. existing_sg_rules]
  73. # SG rules that are meant to be kept get their id back
  74. sg_rules_to_keep = [existing_sg_rules.index(rule) for rule in
  75. existing_sg_rules if rule in current_sg_rules]
  76. for sg_rule in sg_rules_to_keep:
  77. sgr_id = sgr_ids[sg_rule]
  78. existing_sg_rules[sg_rule]['security_group_rule']['id'] = sgr_id
  79. # Delete SG rules that are no longer in the updated policy
  80. sg_rules_to_delete = [existing_sg_rules.index(rule) for rule in
  81. existing_sg_rules if rule not in
  82. current_sg_rules]
  83. for sg_rule in sg_rules_to_delete:
  84. try:
  85. driver_utils.delete_security_group_rule(sgr_ids[sg_rule])
  86. except n_exc.NotFound:
  87. LOG.debug('Trying to delete non existing sg_rule %s', sg_rule)
  88. # Create new rules that weren't already on the security group
  89. sg_rules_to_add = [rule for rule in current_sg_rules if rule not in
  90. existing_sg_rules]
  91. for sg_rule in sg_rules_to_add:
  92. sgr_id = driver_utils.create_security_group_rule(sg_rule)
  93. if sg_rule['security_group_rule'].get('direction') == 'ingress':
  94. for i_rule in i_rules:
  95. if sg_rule == i_rule:
  96. i_rule["security_group_rule"]["id"] = sgr_id
  97. else:
  98. for e_rule in e_rules:
  99. if sg_rule == e_rule:
  100. e_rule["security_group_rule"]["id"] = sgr_id
  101. # Annotate kuryrnetpolicy CRD with current policy and ruleset
  102. pod_selector = policy['spec'].get('podSelector')
  103. driver_utils.patch_kuryr_crd(crd, i_rules, e_rules, pod_selector,
  104. np_spec=policy['spec'])
  105. if existing_pod_selector != pod_selector:
  106. return existing_pod_selector
  107. return False
  108. def create_security_group_rules_from_network_policy(self, policy,
  109. project_id):
  110. """Create initial security group and rules
  111. This method creates the initial security group for hosting security
  112. group rules coming out of network policies' parsing.
  113. """
  114. sg_name = ("sg-" + policy['metadata']['namespace'] + "-" +
  115. policy['metadata']['name'])
  116. security_group_body = {
  117. "security_group":
  118. {
  119. "name": sg_name,
  120. "project_id": project_id,
  121. "description": "Kuryr-Kubernetes NetPolicy SG"
  122. }
  123. }
  124. sg = None
  125. try:
  126. # Create initial security group
  127. sg = self.neutron.create_security_group(body=security_group_body)
  128. sg_id = sg['security_group']['id']
  129. i_rules, e_rules = self.parse_network_policy_rules(policy, sg_id)
  130. for i_rule in i_rules:
  131. sgr_id = driver_utils.create_security_group_rule(i_rule)
  132. i_rule['security_group_rule']['id'] = sgr_id
  133. for e_rule in e_rules:
  134. sgr_id = driver_utils.create_security_group_rule(e_rule)
  135. e_rule['security_group_rule']['id'] = sgr_id
  136. # NOTE(ltomasbo): Add extra SG rule to allow traffic from services
  137. # subnet
  138. svc_cidr = utils.get_subnet_cidr(
  139. config.CONF.neutron_defaults.service_subnet)
  140. svc_rule = {
  141. u'security_group_rule': {
  142. u'ethertype': 'IPv4',
  143. u'security_group_id': sg_id,
  144. u'direction': 'ingress',
  145. u'description': 'Kuryr-Kubernetes NetPolicy SG rule',
  146. u'remote_ip_prefix': svc_cidr
  147. }}
  148. driver_utils.create_security_group_rule(svc_rule)
  149. except (n_exc.NeutronClientException, exceptions.ResourceNotReady):
  150. LOG.exception("Error creating security group for network policy "
  151. " %s", policy['metadata']['name'])
  152. # If there's any issue creating sg rules, remove them
  153. if sg:
  154. self.neutron.delete_security_group(sg['security_group']['id'])
  155. raise
  156. try:
  157. self._add_kuryrnetpolicy_crd(policy, project_id,
  158. sg['security_group']['id'], i_rules,
  159. e_rules)
  160. except exceptions.K8sClientException:
  161. LOG.exception("Rolling back security groups")
  162. # Same with CRD creation
  163. self.neutron.delete_security_group(sg['security_group']['id'])
  164. raise
  165. try:
  166. crd = self.get_kuryrnetpolicy_crd(policy)
  167. self.kubernetes.annotate(policy['metadata']['selfLink'],
  168. {"kuryrnetpolicy_selfLink":
  169. crd['metadata']['selfLink']})
  170. except exceptions.K8sClientException:
  171. LOG.exception('Error annotating network policy')
  172. raise
  173. def _get_pods_ips(self, pod_selector, namespace=None,
  174. namespace_selector=None):
  175. ips = []
  176. matching_pods = {"items": []}
  177. if namespace_selector:
  178. matching_namespaces = driver_utils.get_namespaces(
  179. namespace_selector)
  180. for ns in matching_namespaces.get('items'):
  181. matching_pods = driver_utils.get_pods(pod_selector,
  182. ns['metadata']['name'])
  183. else:
  184. matching_pods = driver_utils.get_pods(pod_selector, namespace)
  185. for pod in matching_pods.get('items'):
  186. if pod['status']['podIP']:
  187. pod_ip = pod['status']['podIP']
  188. ns = pod['metadata']['namespace']
  189. ips.append({'cidr': pod_ip, 'namespace': ns})
  190. return ips
  191. def _get_namespaces_cidr(self, namespace_selector, namespace=None):
  192. cidrs = []
  193. if not namespace_selector and namespace:
  194. ns = self.kubernetes.get(
  195. '{}/namespaces/{}'.format(constants.K8S_API_BASE, namespace))
  196. ns_cidr = driver_utils.get_namespace_subnet_cidr(ns)
  197. cidrs.append({'cidr': ns_cidr, 'namespace': namespace})
  198. else:
  199. matching_namespaces = driver_utils.get_namespaces(
  200. namespace_selector)
  201. for ns in matching_namespaces.get('items'):
  202. # NOTE(ltomasbo): This requires the namespace handler to be
  203. # also enabled
  204. ns_cidr = driver_utils.get_namespace_subnet_cidr(ns)
  205. ns_name = ns['metadata']['name']
  206. cidrs.append({'cidr': ns_cidr, 'namespace': ns_name})
  207. return cidrs
  208. def _parse_selectors(self, rule_block, rule_direction, policy_namespace):
  209. allowed_cidrs = []
  210. allow_all = False
  211. selectors = False
  212. for rule in rule_block.get(rule_direction, []):
  213. namespace_selector = rule.get('namespaceSelector')
  214. pod_selector = rule.get('podSelector')
  215. if namespace_selector == {}:
  216. selectors = True
  217. if pod_selector:
  218. # allow matching pods in all namespaces
  219. allowed_cidrs.extend(self._get_pods_ips(
  220. pod_selector))
  221. else:
  222. # allow from all
  223. allow_all = True
  224. elif namespace_selector:
  225. selectors = True
  226. if pod_selector:
  227. # allow matching pods on matching namespaces
  228. allowed_cidrs.extend(self._get_pods_ips(
  229. pod_selector,
  230. namespace_selector=namespace_selector))
  231. else:
  232. # allow from/to all on the matching namespaces
  233. allowed_cidrs.extend(self._get_namespaces_cidr(
  234. namespace_selector))
  235. else:
  236. if pod_selector == {}:
  237. # allow from/to all pods on the network policy
  238. # namespace
  239. selectors = True
  240. allowed_cidrs.extend(self._get_namespaces_cidr(
  241. None,
  242. namespace=policy_namespace))
  243. elif pod_selector:
  244. # allow matching pods on the network policy
  245. # namespace
  246. selectors = True
  247. allowed_cidrs.extend(self._get_pods_ips(
  248. pod_selector,
  249. namespace=policy_namespace))
  250. return allow_all, selectors, allowed_cidrs
  251. def _parse_sg_rules(self, sg_rule_body_list, direction, policy, sg_id):
  252. rule_list = policy['spec'].get(direction)
  253. if not rule_list:
  254. return
  255. policy_namespace = policy['metadata']['namespace']
  256. rule_direction = 'from'
  257. if direction == 'egress':
  258. rule_direction = 'to'
  259. if rule_list[0] == {}:
  260. LOG.debug('Applying default all open policy from %s',
  261. policy['metadata']['selfLink'])
  262. rule = driver_utils.create_security_group_rule_body(
  263. sg_id, direction, port_range_min=1, port_range_max=65535)
  264. sg_rule_body_list.append(rule)
  265. for rule_block in rule_list:
  266. LOG.debug('Parsing %(dir)s Rule %(rule)s', {'dir': direction,
  267. 'rule': rule_block})
  268. allow_all, selectors, allowed_cidrs = self._parse_selectors(
  269. rule_block, rule_direction, policy_namespace)
  270. if 'ports' in rule_block:
  271. for port in rule_block['ports']:
  272. if allowed_cidrs or allow_all or selectors:
  273. for cidr in allowed_cidrs:
  274. rule = (
  275. driver_utils.create_security_group_rule_body(
  276. sg_id, direction, port.get('port'),
  277. protocol=port.get('protocol'),
  278. cidr=cidr.get('cidr'),
  279. namespace=cidr.get('namespace')))
  280. sg_rule_body_list.append(rule)
  281. if allow_all:
  282. rule = (
  283. driver_utils.create_security_group_rule_body(
  284. sg_id, direction, port.get('port'),
  285. protocol=port.get('protocol')))
  286. sg_rule_body_list.append(rule)
  287. else:
  288. rule = driver_utils.create_security_group_rule_body(
  289. sg_id, direction, port.get('port'),
  290. protocol=port.get('protocol'))
  291. sg_rule_body_list.append(rule)
  292. elif allowed_cidrs or allow_all or selectors:
  293. for cidr in allowed_cidrs:
  294. rule = driver_utils.create_security_group_rule_body(
  295. sg_id, direction,
  296. port_range_min=1,
  297. port_range_max=65535,
  298. cidr=cidr.get('cidr'),
  299. namespace=cidr.get('namespace'))
  300. sg_rule_body_list.append(rule)
  301. if allow_all:
  302. rule = driver_utils.create_security_group_rule_body(
  303. sg_id, direction,
  304. port_range_min=1,
  305. port_range_max=65535)
  306. sg_rule_body_list.append(rule)
  307. else:
  308. LOG.debug('This network policy specifies no %(direction)s '
  309. '%(rule_direction)s and no ports: %(policy)s',
  310. {'direction': direction,
  311. 'rule_direction': rule_direction,
  312. 'policy': policy['metadata']['selfLink']})
  313. def parse_network_policy_rules(self, policy, sg_id):
  314. """Create security group rule bodies out of network policies.
  315. Whenever a notification from the handler 'on-present' method is
  316. received, security group rules are created out of network policies'
  317. ingress and egress ports blocks.
  318. """
  319. LOG.debug('Parsing Network Policy %s' % policy['metadata']['name'])
  320. ingress_sg_rule_body_list = []
  321. egress_sg_rule_body_list = []
  322. self._parse_sg_rules(ingress_sg_rule_body_list, 'ingress', policy,
  323. sg_id)
  324. self._parse_sg_rules(egress_sg_rule_body_list, 'egress', policy,
  325. sg_id)
  326. return ingress_sg_rule_body_list, egress_sg_rule_body_list
  327. def release_network_policy(self, netpolicy_crd):
  328. if netpolicy_crd is not None:
  329. try:
  330. sg_id = netpolicy_crd['spec']['securityGroupId']
  331. self.neutron.delete_security_group(sg_id)
  332. except n_exc.NotFound:
  333. LOG.debug("Security Group not found: %s", sg_id)
  334. except n_exc.Conflict:
  335. LOG.debug("Security Group already in use: %s", sg_id)
  336. # raising ResourceNotReady to retry this action in case ports
  337. # associated to affected pods are not updated on time, i.e.,
  338. # they are still using the security group to be removed
  339. raise exceptions.ResourceNotReady(sg_id)
  340. except n_exc.NeutronClientException:
  341. LOG.exception("Error deleting security group %s.", sg_id)
  342. raise
  343. self._del_kuryrnetpolicy_crd(
  344. netpolicy_crd['metadata']['name'],
  345. netpolicy_crd['metadata']['namespace'])
  346. def get_kuryrnetpolicy_crd(self, policy):
  347. netpolicy_crd_name = "np-" + policy['metadata']['name']
  348. netpolicy_crd_namespace = policy['metadata']['namespace']
  349. try:
  350. netpolicy_crd = self.kubernetes.get(
  351. '{}/{}/kuryrnetpolicies/{}'.format(
  352. constants.K8S_API_CRD_NAMESPACES, netpolicy_crd_namespace,
  353. netpolicy_crd_name))
  354. except exceptions.K8sResourceNotFound:
  355. return None
  356. except exceptions.K8sClientException:
  357. LOG.exception("Kubernetes Client Exception.")
  358. raise
  359. return netpolicy_crd
  360. def knps_on_namespace(self, namespace):
  361. try:
  362. netpolicy_crds = self.kubernetes.get(
  363. '{}/{}/kuryrnetpolicies'.format(
  364. constants.K8S_API_CRD_NAMESPACES,
  365. namespace))
  366. except exceptions.K8sClientException:
  367. LOG.exception("Kubernetes Client Exception.")
  368. raise
  369. if netpolicy_crds.get('items'):
  370. return True
  371. return False
  372. def _add_kuryrnetpolicy_crd(self, policy, project_id, sg_id, i_rules,
  373. e_rules):
  374. networkpolicy_name = policy['metadata']['name']
  375. netpolicy_crd_name = "np-" + networkpolicy_name
  376. namespace = policy['metadata']['namespace']
  377. pod_selector = policy['spec'].get('podSelector')
  378. netpolicy_crd = {
  379. 'apiVersion': 'openstack.org/v1',
  380. 'kind': constants.K8S_OBJ_KURYRNETPOLICY,
  381. 'metadata': {
  382. 'name': netpolicy_crd_name,
  383. 'namespace': namespace,
  384. 'annotations': {
  385. 'networkpolicy_name': networkpolicy_name,
  386. 'networkpolicy_namespace': namespace,
  387. 'networkpolicy_uid': policy['metadata']['uid'],
  388. },
  389. },
  390. 'spec': {
  391. 'securityGroupName': "sg-" + networkpolicy_name,
  392. 'securityGroupId': sg_id,
  393. 'ingressSgRules': i_rules,
  394. 'egressSgRules': e_rules,
  395. 'podSelector': pod_selector,
  396. 'networkpolicy_spec': policy['spec']
  397. },
  398. }
  399. try:
  400. LOG.debug("Creating KuryrNetPolicy CRD %s" % netpolicy_crd)
  401. kubernetes_post = '{}/{}/kuryrnetpolicies'.format(
  402. constants.K8S_API_CRD_NAMESPACES,
  403. namespace)
  404. self.kubernetes.post(kubernetes_post, netpolicy_crd)
  405. except exceptions.K8sClientException:
  406. LOG.exception("Kubernetes Client Exception creating kuryrnetpolicy"
  407. " CRD. %s" % exceptions.K8sClientException)
  408. raise
  409. return netpolicy_crd
  410. def _del_kuryrnetpolicy_crd(self, netpolicy_crd_name,
  411. netpolicy_crd_namespace):
  412. try:
  413. LOG.debug("Deleting KuryrNetPolicy CRD %s" % netpolicy_crd_name)
  414. self.kubernetes.delete('{}/{}/kuryrnetpolicies/{}'.format(
  415. constants.K8S_API_CRD_NAMESPACES,
  416. netpolicy_crd_namespace,
  417. netpolicy_crd_name))
  418. except exceptions.K8sClientException:
  419. LOG.exception("Kubernetes Client Exception deleting kuryrnetpolicy"
  420. " CRD.")
  421. raise
  422. def affected_pods(self, policy, selector=None):
  423. if selector:
  424. pod_selector = selector
  425. else:
  426. pod_selector = policy['spec'].get('podSelector')
  427. if pod_selector:
  428. policy_namespace = policy['metadata']['namespace']
  429. pods = driver_utils.get_pods(pod_selector, policy_namespace)
  430. return pods.get('items')
  431. else:
  432. # NOTE(ltomasbo): It affects all the pods on the namespace
  433. return self.namespaced_pods(policy)
  434. def namespaced_pods(self, policy):
  435. pod_namespace = policy['metadata']['namespace']
  436. pods = self.kubernetes.get('{}/namespaces/{}/pods'.format(
  437. constants.K8S_API_BASE, pod_namespace))
  438. return pods.get('items')