diff -Nru horizon-13.0.2/AUTHORS horizon-13.0.3/AUTHORS --- horizon-13.0.2/AUTHORS 2019-05-09 22:31:37.000000000 +0000 +++ horizon-13.0.3/AUTHORS 2019-10-22 20:10:09.000000000 +0000 @@ -595,6 +595,7 @@ Shaoquan Chen Sharat Sharma Shilla Saebi +Shilpa Devharakar Shinya Kawabata Shu Muto Shu Muto @@ -747,6 +748,7 @@ eric ericpeterson-l facundo Farias +francotseng gaofei gavin.nie@cn.ibm.com gecong1973 diff -Nru horizon-13.0.2/ChangeLog horizon-13.0.3/ChangeLog --- horizon-13.0.2/ChangeLog 2019-05-09 22:31:37.000000000 +0000 +++ horizon-13.0.3/ChangeLog 2019-10-22 20:10:08.000000000 +0000 @@ -1,6 +1,15 @@ CHANGES ======= +13.0.3 +------ + +* Fix listing security groups when no rules +* Allow creating ICMPV6 rules +* Add Allowed Address Pair/Delete buttons are only visible to admin +* Fix image description field +* Complete angular translation extract pattern + 13.0.2 ------ diff -Nru horizon-13.0.2/debian/changelog horizon-13.0.3/debian/changelog --- horizon-13.0.2/debian/changelog 2020-04-27 17:29:24.000000000 +0000 +++ horizon-13.0.3/debian/changelog 2020-08-28 12:02:38.000000000 +0000 @@ -1,3 +1,11 @@ +horizon (3:13.0.3-0ubuntu1) bionic; urgency=medium + + * d/watch: Update to point at opendev. + * New stable point release for OpenStack Queens (LP: #1893234). + * d/p/lp1840465.patch: Removed: Fixed in new upstream point release. + + -- Chris MacNaughton Fri, 28 Aug 2020 12:02:38 +0000 + horizon (3:13.0.2-0ubuntu3) bionic; urgency=medium * d/p/Avoid_forced_logout_when_403_error_encountered.patch: diff -Nru horizon-13.0.2/debian/patches/lp1840465.patch horizon-13.0.3/debian/patches/lp1840465.patch --- horizon-13.0.2/debian/patches/lp1840465.patch 2020-04-27 17:29:24.000000000 +0000 +++ horizon-13.0.3/debian/patches/lp1840465.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +0,0 @@ -From 9d73ebee7a1ce442f57fad319d6ca6b354ffe171 Mon Sep 17 00:00:00 2001 -From: Akihiro Motoki -Date: Mon, 19 Aug 2019 16:35:42 +0900 -Subject: [PATCH] Fix listing security groups when no rules - -When listing security groups in the dashboard and -one or more security groups had no rules it failed -because python throws a KeyError. - -This commit changes the neutron API wrapper in horizon -to ensure ensure rule information in SG always exists. - -Closes-Bug: #1840465 -Co-Authored-By: Tobias Urdin -Change-Id: I6e05a7dc6b6655514ee2bff6bd327da86f13900a -(cherry picked from commit cdb191ec83f86dffade56be07ca53077d7c78b14) -(cherry picked from commit 205c4465cdf8112779e8eb5f1618dcfc1b11e305) ---- - openstack_dashboard/api/neutron.py | 2 ++ - openstack_dashboard/test/test_data/neutron_data.py | 12 +++++++++--- - openstack_dashboard/test/unit/api/test_neutron.py | 4 +++- - ...-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml | 5 +++++ - 4 files changed, 19 insertions(+), 4 deletions(-) - create mode 100644 releasenotes/notes/security-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml - -Index: horizon-13.0.2/openstack_dashboard/api/neutron.py -=================================================================== ---- horizon-13.0.2.orig/openstack_dashboard/api/neutron.py -+++ horizon-13.0.2/openstack_dashboard/api/neutron.py -@@ -205,6 +205,8 @@ class SecurityGroup(NeutronAPIDictWrappe - def __init__(self, sg, sg_dict=None): - if sg_dict is None: - sg_dict = {sg['id']: sg['name']} -+ if 'security_group_rules' not in sg: -+ sg['security_group_rules'] = [] - sg['rules'] = [SecurityGroupRule(rule, sg_dict) - for rule in sg['security_group_rules']] - super(SecurityGroup, self).__init__(sg) -Index: horizon-13.0.2/openstack_dashboard/test/test_data/neutron_data.py -=================================================================== ---- horizon-13.0.2.orig/openstack_dashboard/test/test_data/neutron_data.py -+++ horizon-13.0.2/openstack_dashboard/test/test_data/neutron_data.py -@@ -496,6 +496,10 @@ def data(TEST): - 'description': 'NotDefault', - 'id': '443a4d7a-4bd2-4474-9a77-02b35c9f8c95', - 'name': 'another_group'} -+ sec_group_empty = {'tenant_id': '1', -+ 'description': 'SG without rules', -+ 'id': 'f205f3bc-d402-4e40-b004-c62401e19b4b', -+ 'name': 'empty_group'} - - def add_rule_to_group(secgroup, default_only=True): - rule_egress_ipv4 = { -@@ -557,18 +561,20 @@ def data(TEST): - add_rule_to_group(sec_group_1, default_only=False) - add_rule_to_group(sec_group_2) - add_rule_to_group(sec_group_3) -+ # NOTE: sec_group_empty is a SG without rules, -+ # so we don't call add_rule_to_group. - -- groups = [sec_group_1, sec_group_2, sec_group_3] -+ groups = [sec_group_1, sec_group_2, sec_group_3, sec_group_empty] - sg_name_dict = dict([(sg['id'], sg['name']) for sg in groups]) - for sg in groups: - # Neutron API. - TEST.api_security_groups.add(sg) -- for rule in sg['security_group_rules']: -+ for rule in sg.get('security_group_rules', []): - TEST.api_security_group_rules.add(copy.copy(rule)) - # OpenStack Dashboard internaly API. - TEST.security_groups.add( - neutron.SecurityGroup(copy.deepcopy(sg), sg_name_dict)) -- for rule in sg['security_group_rules']: -+ for rule in sg.get('security_group_rules', []): - TEST.security_group_rules.add( - neutron.SecurityGroupRule(copy.copy(rule), sg_name_dict)) - -Index: horizon-13.0.2/openstack_dashboard/test/unit/api/test_neutron.py -=================================================================== ---- horizon-13.0.2.orig/openstack_dashboard/test/unit/api/test_neutron.py -+++ horizon-13.0.2/openstack_dashboard/test/unit/api/test_neutron.py -@@ -963,7 +963,9 @@ class NeutronApiSecurityGroupTests(Neutr - def _cmp_sg(self, exp_sg, ret_sg): - self.assertEqual(exp_sg['id'], ret_sg.id) - self.assertEqual(exp_sg['name'], ret_sg.name) -- exp_rules = exp_sg['security_group_rules'] -+ # When a SG has no rules, neutron API does not contain -+ # 'security_group_rules' field, so .get() method needs to be used. -+ exp_rules = exp_sg.get('security_group_rules', []) - self.assertEqual(len(exp_rules), len(ret_sg.rules)) - for (exprule, retrule) in six.moves.zip(exp_rules, ret_sg.rules): - self._cmp_sg_rule(exprule, retrule) -Index: horizon-13.0.2/releasenotes/notes/security-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml -=================================================================== ---- /dev/null -+++ horizon-13.0.2/releasenotes/notes/security-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml -@@ -0,0 +1,5 @@ -+--- -+fixes: -+ - | -+ [:bug:`1840465`] Fixed a bug where listing security groups did not work -+ if one or more security groups had no rules in them. diff -Nru horizon-13.0.2/debian/patches/series horizon-13.0.3/debian/patches/series --- horizon-13.0.2/debian/patches/series 2020-04-27 17:29:24.000000000 +0000 +++ horizon-13.0.3/debian/patches/series 2020-08-28 12:02:38.000000000 +0000 @@ -4,5 +4,4 @@ ubuntu_settings.patch embedded-xstatic.patch add-juju-environment-download.patch -lp1840465.patch Avoid_forced_logout_when_403_error_encountered.patch diff -Nru horizon-13.0.2/debian/watch horizon-13.0.3/debian/watch --- horizon-13.0.2/debian/watch 2020-04-27 17:29:24.000000000 +0000 +++ horizon-13.0.3/debian/watch 2020-08-28 12:02:38.000000000 +0000 @@ -1,3 +1,3 @@ version=3 opts="uversionmangle=s/\.([a-zA-Z])/~$1/;s/%7E/~/;s/\.0b/~b/;s/\.0rc/~rc/" \ - http://tarballs.openstack.org/horizon horizon-(13\.\d.*)\.tar\.gz + https://tarballs.opendev.org/openstack/horizon horizon-(13\.\d.*)\.tar\.gz diff -Nru horizon-13.0.2/horizon/test/unit/utils/test_babel_extract_angular.py horizon-13.0.3/horizon/test/unit/utils/test_babel_extract_angular.py --- horizon-13.0.2/horizon/test/unit/utils/test_babel_extract_angular.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/horizon/test/unit/utils/test_babel_extract_angular.py 2019-10-22 20:09:16.000000000 +0000 @@ -135,6 +135,15 @@ {$'some other thing'$}

{$'"it\\'s awesome"'|translate$}

{$"oh \\"hello\\" there"|translate$}

+ {$::'hello colon1' | translate $} +

{$ ::'hello colon2' |translate$}

+

{$ :: 'hello colon3'| translate$}

+ something {$::'hello colon4'|translate$} something
+            {$ ::'hello colon5' | translate$} + {::$expr()|translate$} + {$::'some other thing'$} +

{$:: '"it\\'s awesome"'|translate$}

+

{$ :: "oh \\"hello\\" there" | translate$}

""" ) @@ -147,6 +156,13 @@ (4, u'gettext', 'hello world4', []), (8, u'gettext', '"it\\\'s awesome"', []), (9, u'gettext', 'oh \\"hello\\" there', []), + (10, u'gettext', u'hello colon1', []), + (11, u'gettext', u'hello colon2', []), + (12, u'gettext', u'hello colon3', []), + (13, u'gettext', u'hello colon4', []), + (13, u'gettext', u'hello colon5', []), + (17, u'gettext', u'"it\\\'s awesome"', []), + (18, u'gettext', u'oh \\"hello\\" there', []), ], messages) diff -Nru horizon-13.0.2/horizon/utils/babel_extract_angular.py horizon-13.0.3/horizon/utils/babel_extract_angular.py --- horizon-13.0.2/horizon/utils/babel_extract_angular.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/horizon/utils/babel_extract_angular.py 2019-10-22 20:09:17.000000000 +0000 @@ -20,7 +20,7 @@ # regex to find filter translation expressions filter_regex = re.compile( - r"""{\$\s*('([^']|\\')+'|"([^"]|\\")+")\s*\|\s*translate\s*\$}""" + r"""{\$\s*(::)?\s*('([^']|\\')+'|"([^"]|\\")+")\s*\|\s*translate\s*\$}""" ) # browser innerHTML decodes some html entities automatically, so when @@ -48,6 +48,8 @@ {$ 'content' | translate $} The string will be translated, minus expression handling (i.e. just bare strings are allowed.) + {$ ::'content' | translate $} + The string will be translated. As above. """ def __init__(self): @@ -93,7 +95,7 @@ for match in filter_regex.findall(attr[1]): if match: self.strings.append( - (self.line, u'gettext', match[0][1:-1], []) + (self.line, u'gettext', match[1][1:-1], []) ) def handle_data(self, data): @@ -102,7 +104,7 @@ else: for match in filter_regex.findall(data): self.strings.append( - (self.line, u'gettext', match[0][1:-1], []) + (self.line, u'gettext', match[1][1:-1], []) ) def handle_entityref(self, name): diff -Nru horizon-13.0.2/horizon.egg-info/pbr.json horizon-13.0.3/horizon.egg-info/pbr.json --- horizon-13.0.2/horizon.egg-info/pbr.json 2019-05-09 22:31:37.000000000 +0000 +++ horizon-13.0.3/horizon.egg-info/pbr.json 2019-10-22 20:10:09.000000000 +0000 @@ -1 +1 @@ -{"git_version": "d9dc8340f", "is_release": true} \ No newline at end of file +{"git_version": "919870a13", "is_release": true} \ No newline at end of file diff -Nru horizon-13.0.2/horizon.egg-info/PKG-INFO horizon-13.0.3/horizon.egg-info/PKG-INFO --- horizon-13.0.2/horizon.egg-info/PKG-INFO 2019-05-09 22:31:37.000000000 +0000 +++ horizon-13.0.3/horizon.egg-info/PKG-INFO 2019-10-22 20:10:09.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: horizon -Version: 13.0.2 +Version: 13.0.3 Summary: OpenStack Dashboard Home-page: https://docs.openstack.org/horizon/latest/ Author: OpenStack diff -Nru horizon-13.0.2/horizon.egg-info/SOURCES.txt horizon-13.0.3/horizon.egg-info/SOURCES.txt --- horizon-13.0.2/horizon.egg-info/SOURCES.txt 2019-05-09 22:31:39.000000000 +0000 +++ horizon-13.0.3/horizon.egg-info/SOURCES.txt 2019-10-22 20:10:10.000000000 +0000 @@ -2626,6 +2626,7 @@ releasenotes/notes/heat-panel-splitout-b609b157aa4bf29b.yaml releasenotes/notes/horizon-without-nova-3cd0a84109ed2187.yaml releasenotes/notes/hz-select-fixes-c9bfe6a53e0daa20.yaml +releasenotes/notes/image-description-3fc00c02f46a80c7.yaml releasenotes/notes/image-panel-switch-38e9d3716451f9e3.yaml releasenotes/notes/introduce_default_service_regions_config-26a41e0d06582d7a.yaml releasenotes/notes/ip-availability-be217ba59cc02b40.yaml @@ -2659,6 +2660,7 @@ releasenotes/notes/resource-directives-44629f1116545141.yaml releasenotes/notes/security-group-associate-per-port-c81ca7beb7dca409.yaml releasenotes/notes/security-group-in-port-detail-10a7f5d6d50d1571.yaml +releasenotes/notes/security-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml releasenotes/notes/security-group-rule-wildcard-protocol-and-port-support-7dd6f5acfaba55ba.yaml releasenotes/notes/setting-OVERVIEW_DAYS_RANGE-9b87e8b077952a32.yaml releasenotes/notes/setting-openstack-endpoint-type-ebdeda92ba0d1587.yaml diff -Nru horizon-13.0.2/openstack_dashboard/api/neutron.py horizon-13.0.3/openstack_dashboard/api/neutron.py --- horizon-13.0.2/openstack_dashboard/api/neutron.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/api/neutron.py 2019-10-22 20:09:17.000000000 +0000 @@ -205,6 +205,8 @@ def __init__(self, sg, sg_dict=None): if sg_dict is None: sg_dict = {sg['id']: sg['name']} + if 'security_group_rules' not in sg: + sg['security_group_rules'] = [] sg['rules'] = [SecurityGroupRule(rule, sg_dict) for rule in sg['security_group_rules']] super(SecurityGroup, self).__init__(sg) diff -Nru horizon-13.0.2/openstack_dashboard/dashboards/admin/networks/ports/tests.py horizon-13.0.3/openstack_dashboard/dashboards/admin/networks/ports/tests.py --- horizon-13.0.2/openstack_dashboard/dashboards/admin/networks/ports/tests.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/dashboards/admin/networks/ports/tests.py 2019-10-22 20:09:17.000000000 +0000 @@ -15,6 +15,7 @@ from django.core.urlresolvers import reverse from django import http +from django.test.utils import override_settings from mox3.mox import IsA @@ -594,3 +595,75 @@ res = self.client.post(url, form_data) self.assertRedirectsNoFollow(res, url) + + @override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check') + @test.create_stubs({api.neutron: ('port_get', + 'network_get', + 'is_extension_supported')}) + def test_add_allowed_address_pair_button_shown(self): + port = self.ports.first() + network_id = self.networks.first().id + api.neutron.port_get(IsA(http.HttpRequest), port.id) \ + .AndReturn(self.ports.first()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning') \ + .MultipleTimes().AndReturn(False) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(True) + api.neutron.network_get(IsA(http.HttpRequest), network_id) \ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + url = reverse('horizon:project:networks:ports:addallowedaddresspairs', + args=[port.id]) + classes = 'btn data-table-action btn-default ajax-modal' + link_name = "Add Allowed Address Pair" + + expected_string = \ + '' \ + ' %s' \ + % (classes, url, link_name) + + res = self.client.get(reverse('horizon:project:networks:ports:detail', + args=[port.id])) + + self.assertTemplateUsed(res, 'horizon/common/_detail_tab_group.html') + self.assertIn(expected_string, res.context_data['tab_group'].render()) + + @override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check') + @test.create_stubs({api.neutron: ('port_get', + 'network_get', + 'is_extension_supported')}) + def test_delete_address_pair_button_shown(self): + port = self.ports.first() + network_id = self.networks.first().id + api.neutron.port_get(IsA(http.HttpRequest), port.id) \ + .AndReturn(self.ports.first()) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning') \ + .MultipleTimes().AndReturn(False) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(True) + api.neutron.network_get(IsA(http.HttpRequest), network_id) \ + .AndReturn(self.networks.first()) + self.mox.ReplayAll() + + classes = 'data-table-action btn-danger btn' + + expected_string = \ + '' \ + % (classes) + + res = self.client.get(reverse( + 'horizon:project:networks:ports:detail', args=[port.id])) + + self.assertTemplateUsed(res, 'horizon/common/_detail_tab_group.html') + self.assertIn(expected_string, res.context_data['tab_group'].render()) diff -Nru horizon-13.0.2/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tables.py horizon-13.0.3/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tables.py --- horizon-13.0.2/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tables.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/dashboards/project/networks/ports/extensions/allowed_address_pairs/tables.py 2019-10-22 20:09:17.000000000 +0000 @@ -39,6 +39,14 @@ ("network", "update_port:allowed_address_pairs"), ) + def get_policy_target(self, request, datum=None): + policy_target = super(AddAllowedAddressPair, self).\ + get_policy_target(request, datum) + policy_target["network:tenant_id"] = ( + self.table.kwargs['port'].tenant_id) + + return policy_target + def get_link_url(self, port=None): if port: return reverse(self.url, args=(port.id,)) @@ -68,6 +76,14 @@ ("network", "update_port:allowed_address_pairs"), ) + def get_policy_target(self, request, datum=None): + policy_target = super(DeleteAllowedAddressPair, self).\ + get_policy_target(request, datum) + policy_target["network:tenant_id"] = ( + self.table.kwargs['port'].tenant_id) + + return policy_target + def delete(self, request, ip_address): try: port_id = self.table.kwargs['port_id'] diff -Nru horizon-13.0.2/openstack_dashboard/dashboards/project/networks/ports/tests.py horizon-13.0.3/openstack_dashboard/dashboards/project/networks/ports/tests.py --- horizon-13.0.2/openstack_dashboard/dashboards/project/networks/ports/tests.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/dashboards/project/networks/ports/tests.py 2019-10-22 20:09:17.000000000 +0000 @@ -17,9 +17,13 @@ from django.core.urlresolvers import reverse from django import http +from django.test.utils import override_settings +import mock from mox3.mox import IsA +from openstack_auth import utils as auth_utils + from horizon.workflows import views from openstack_dashboard import api @@ -263,6 +267,84 @@ address_pairs = res.context['allowed_address_pairs_table'].data self.assertItemsEqual(port.allowed_address_pairs, address_pairs) + @override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check') + @test.create_stubs({api.neutron: ('port_get', + 'network_get', + 'is_extension_supported')}) + def test_add_allowed_address_pair_button_shown_to_network_owner(self): + port = self.ports.first() + + api.neutron.port_get(IsA(http.HttpRequest), port.id) \ + .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(True) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning') \ + .MultipleTimes().AndReturn(False) + self.mox.ReplayAll() + + url = reverse('horizon:project:networks:ports:addallowedaddresspairs', + args=[port.id]) + classes = 'btn data-table-action btn-default ajax-modal' + link_name = "Add Allowed Address Pair" + + expected_string = \ + '' \ + ' %s' \ + % (classes, url, link_name) + + res = self.client.get(reverse('horizon:project:networks:ports:detail', + args=[port.id])) + + self.assertTemplateUsed(res, 'horizon/common/_detail_tab_group.html') + self.assertIn(expected_string, res.context_data['tab_group'].render()) + + @override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check') + @test.create_stubs({api.neutron: ('network_get', + 'port_get', + 'is_extension_supported', + 'security_group_list',)}) + def test_add_allowed_address_pair_button_disabled_to_other_tenant(self): + # Current user tenant_id is 1 so select port whose tenant_id is + # other than 1 for checking "Add Allowed Address Pair" button is not + # displayed on the screen. + user = auth_utils.get_user(self.request) + + # select port such that tenant_id is different from user's tenant_id. + port = [p for p in self.ports.list() + if p.tenant_id != user.tenant_id][0] + + api.neutron.port_get(IsA(http.HttpRequest), port.id) \ + .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(False) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning') \ + .MultipleTimes().AndReturn(False) + self.mox.ReplayAll() + + with mock.patch('openstack_auth.utils.get_user', return_value=user): + url = reverse( + 'horizon:project:networks:ports:addallowedaddresspairs', + args=[port.id]) + classes = 'btn data-table-action btn-default ajax-modal' + link_name = "Add Allowed Address Pair" + + expected_string = \ + '' \ + ' %s' \ + % (classes, url, link_name) + + res = self.client.get(reverse( + 'horizon:project:networks:ports:detail', args=[port.id])) + + self.assertNotIn( + expected_string, res.context_data['tab_group'].render()) + @test.create_stubs({api.neutron: ('port_get', 'port_update')}) def test_port_add_allowed_address_pair(self): detail_path = 'horizon:project:networks:ports:detail' @@ -319,6 +401,83 @@ self.assertFormErrors(res, 1) self.assertContains(res, "Incorrect format for IP address") + @override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check') + @test.create_stubs({api.neutron: ('port_get', + 'network_get', + 'is_extension_supported')}) + def test_delete_address_pair_button_shown_to_network_owner(self): + port = self.ports.first() + + api.neutron.port_get(IsA(http.HttpRequest), port.id) \ + .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(True) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning') \ + .MultipleTimes().AndReturn(False) + self.mox.ReplayAll() + + classes = 'data-table-action btn-danger btn' + + expected_string = \ + '' \ + % (classes) + + res = self.client.get(reverse( + 'horizon:project:networks:ports:detail', args=[port.id])) + + self.assertTemplateUsed(res, 'horizon/common/_detail_tab_group.html') + self.assertIn(expected_string, res.context_data['tab_group'].render()) + + @override_settings(POLICY_CHECK_FUNCTION='openstack_auth.policy.check') + @test.create_stubs({api.neutron: ('network_get', + 'port_get', + 'is_extension_supported', + 'security_group_list',)}) + def test_delete_address_pair_button_disabled_to_other_tenant(self): + # Current user tenant_id is 1 so select port whose tenant_id is + # other than 1 for checking "Delete Allowed Address Pair" button is + # not displayed on the screen. + user = auth_utils.get_user(self.request) + + # select port such that tenant_id is different from user's tenant_id. + port = [p for p in self.ports.list() + if p.tenant_id != user.tenant_id][0] + + api.neutron.port_get(IsA(http.HttpRequest), port.id) \ + .AndReturn(port) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'allowed-address-pairs') \ + .MultipleTimes().AndReturn(False) + api.neutron.is_extension_supported(IsA(http.HttpRequest), + 'mac-learning') \ + .MultipleTimes().AndReturn(False) + self.mox.ReplayAll() + + with mock.patch('openstack_auth.utils.get_user', return_value=user): + classes = 'data-table-action btn-danger btn' + + expected_string = \ + '' % (classes) + + res = self.client.get(reverse( + 'horizon:project:networks:ports:detail', args=[port.id])) + + self.assertNotIn( + expected_string, res.context_data['tab_group'].render()) + @test.create_stubs({api.neutron: ('port_get', 'port_update', 'is_extension_supported',)}) def test_port_remove_allowed_address_pair(self): diff -Nru horizon-13.0.2/openstack_dashboard/dashboards/project/security_groups/forms.py horizon-13.0.3/openstack_dashboard/dashboards/project/security_groups/forms.py --- horizon-13.0.2/openstack_dashboard/dashboards/project/security_groups/forms.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/dashboards/project/security_groups/forms.py 2019-10-22 20:09:17.000000000 +0000 @@ -377,6 +377,35 @@ else: self._apply_rule_menu(cleaned_data, rule_menu) + def _adjust_ip_protocol_of_icmp(self, data): + # Note that this needs to be called after IPv4/IPv6 is determined. + try: + ip_protocol = int(data['ip_protocol']) + except ValueError: + # string representation of IP protocol + ip_protocol = data['ip_protocol'] + is_ipv6 = data['ethertype'] == 'IPv6' + + if isinstance(ip_protocol, int): + # When IP protocol number is specified, we assume a user + # knows more detail on IP protocol number, + # so a warning message on a mismatch between IP proto number + # and IP version is displayed. + if is_ipv6 and ip_protocol == 1: + msg = _('58 (ipv6-icmp) should be specified for IPv6 ' + 'instead of 1.') + self._errors['ip_protocol'] = self.error_class([msg]) + elif not is_ipv6 and ip_protocol == 58: + msg = _('1 (icmp) should be specified for IPv4 ' + 'instead of 58.') + self._errors['ip_protocol'] = self.error_class([msg]) + else: + # ICMPv6 uses different an IP protocol name and number. + # To allow 'icmp' for both IPv4 and IPv6 in the form, + # we need to replace 'icmp' with 'ipv6-icmp' based on IP version. + if is_ipv6 and ip_protocol == 'icmp': + data['ip_protocol'] = 'ipv6-icmp' + def clean(self): cleaned_data = super(AddRule, self).clean() @@ -411,6 +440,8 @@ ip_ver = netaddr.IPNetwork(cidr).version cleaned_data['ethertype'] = 'IPv6' if ip_ver == 6 else 'IPv4' + self._adjust_ip_protocol_of_icmp(cleaned_data) + return cleaned_data def handle(self, request, data): diff -Nru horizon-13.0.2/openstack_dashboard/static/app/core/images/actions/edit.action.service.js horizon-13.0.3/openstack_dashboard/static/app/core/images/actions/edit.action.service.js --- horizon-13.0.2/openstack_dashboard/static/app/core/images/actions/edit.action.service.js 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/static/app/core/images/actions/edit.action.service.js 2019-10-22 20:09:16.000000000 +0000 @@ -103,7 +103,8 @@ .then(onMetadataGet); function onMetadataGet(response) { - var updated = metadata; + var updated = metadata || Object(); + updated.description = image.properties.description; var removed = angular.copy(response.data); angular.forEach(updated, function(value, key) { delete removed[key]; diff -Nru horizon-13.0.2/openstack_dashboard/static/app/core/images/actions/edit.action.service.spec.js horizon-13.0.3/openstack_dashboard/static/app/core/images/actions/edit.action.service.spec.js --- horizon-13.0.2/openstack_dashboard/static/app/core/images/actions/edit.action.service.spec.js 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/static/app/core/images/actions/edit.action.service.spec.js 2019-10-22 20:09:16.000000000 +0000 @@ -19,7 +19,7 @@ describe('horizon.app.core.images.actions.edit.service', function() { var service, $scope, $q, deferred, $timeout, updateImageDeferred; - var image = {id: 1, name: 'Original'}; + var image = {id: 1, name: 'Original', properties: {description: 'bla-bla'}}; var existingMetadata = {p1: '1', p2: '2'}; var metadataService = { diff -Nru horizon-13.0.2/openstack_dashboard/test/test_data/neutron_data.py horizon-13.0.3/openstack_dashboard/test/test_data/neutron_data.py --- horizon-13.0.2/openstack_dashboard/test/test_data/neutron_data.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/test/test_data/neutron_data.py 2019-10-22 20:09:17.000000000 +0000 @@ -496,6 +496,10 @@ 'description': 'NotDefault', 'id': '443a4d7a-4bd2-4474-9a77-02b35c9f8c95', 'name': 'another_group'} + sec_group_empty = {'tenant_id': '1', + 'description': 'SG without rules', + 'id': 'f205f3bc-d402-4e40-b004-c62401e19b4b', + 'name': 'empty_group'} def add_rule_to_group(secgroup, default_only=True): rule_egress_ipv4 = { @@ -557,18 +561,20 @@ add_rule_to_group(sec_group_1, default_only=False) add_rule_to_group(sec_group_2) add_rule_to_group(sec_group_3) + # NOTE: sec_group_empty is a SG without rules, + # so we don't call add_rule_to_group. - groups = [sec_group_1, sec_group_2, sec_group_3] + groups = [sec_group_1, sec_group_2, sec_group_3, sec_group_empty] sg_name_dict = dict([(sg['id'], sg['name']) for sg in groups]) for sg in groups: # Neutron API. TEST.api_security_groups.add(sg) - for rule in sg['security_group_rules']: + for rule in sg.get('security_group_rules', []): TEST.api_security_group_rules.add(copy.copy(rule)) # OpenStack Dashboard internaly API. TEST.security_groups.add( neutron.SecurityGroup(copy.deepcopy(sg), sg_name_dict)) - for rule in sg['security_group_rules']: + for rule in sg.get('security_group_rules', []): TEST.security_group_rules.add( neutron.SecurityGroupRule(copy.copy(rule), sg_name_dict)) diff -Nru horizon-13.0.2/openstack_dashboard/test/unit/api/test_neutron.py horizon-13.0.3/openstack_dashboard/test/unit/api/test_neutron.py --- horizon-13.0.2/openstack_dashboard/test/unit/api/test_neutron.py 2019-05-09 22:29:46.000000000 +0000 +++ horizon-13.0.3/openstack_dashboard/test/unit/api/test_neutron.py 2019-10-22 20:09:17.000000000 +0000 @@ -963,7 +963,9 @@ def _cmp_sg(self, exp_sg, ret_sg): self.assertEqual(exp_sg['id'], ret_sg.id) self.assertEqual(exp_sg['name'], ret_sg.name) - exp_rules = exp_sg['security_group_rules'] + # When a SG has no rules, neutron API does not contain + # 'security_group_rules' field, so .get() method needs to be used. + exp_rules = exp_sg.get('security_group_rules', []) self.assertEqual(len(exp_rules), len(ret_sg.rules)) for (exprule, retrule) in six.moves.zip(exp_rules, ret_sg.rules): self._cmp_sg_rule(exprule, retrule) diff -Nru horizon-13.0.2/PKG-INFO horizon-13.0.3/PKG-INFO --- horizon-13.0.2/PKG-INFO 2019-05-09 22:31:40.000000000 +0000 +++ horizon-13.0.3/PKG-INFO 2019-10-22 20:10:11.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: horizon -Version: 13.0.2 +Version: 13.0.3 Summary: OpenStack Dashboard Home-page: https://docs.openstack.org/horizon/latest/ Author: OpenStack diff -Nru horizon-13.0.2/releasenotes/notes/image-description-3fc00c02f46a80c7.yaml horizon-13.0.3/releasenotes/notes/image-description-3fc00c02f46a80c7.yaml --- horizon-13.0.2/releasenotes/notes/image-description-3fc00c02f46a80c7.yaml 1970-01-01 00:00:00.000000000 +0000 +++ horizon-13.0.3/releasenotes/notes/image-description-3fc00c02f46a80c7.yaml 2019-10-22 20:09:16.000000000 +0000 @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix an error on image description field when it is changed + in the Angularized panel [:bug: `1779879`] diff -Nru horizon-13.0.2/releasenotes/notes/security-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml horizon-13.0.3/releasenotes/notes/security-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml --- horizon-13.0.2/releasenotes/notes/security-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml 1970-01-01 00:00:00.000000000 +0000 +++ horizon-13.0.3/releasenotes/notes/security-group-no-rules-list-bugfix-b77ab5aff1d3e45e.yaml 2019-10-22 20:09:16.000000000 +0000 @@ -0,0 +1,5 @@ +--- +fixes: + - | + [:bug:`1840465`] Fixed a bug where listing security groups did not work + if one or more security groups had no rules in them.