diff -Nru horizon-19.2.0+git2021072116.b58ac2894/AUTHORS horizon-20.1.0/AUTHORS --- horizon-19.2.0+git2021072116.b58ac2894/AUTHORS 2021-07-21 20:46:27.000000000 +0000 +++ horizon-20.1.0/AUTHORS 2021-09-23 09:32:33.000000000 +0000 @@ -454,6 +454,7 @@ Luong Anh Tuan Majdi Lada Malini Bhandaru +Manpreet Kaur MaoyangLiu Marc Fouché Marc Methot @@ -738,6 +739,7 @@ Tyler Smith Tyr Johanson Tzu-Mainn Chen +Vadym Markov Vahid Hashemian Val W Valerie Roske @@ -934,6 +936,7 @@ shlo shlo shu-mutou +shubhamdang shutingm simon sleepsonthefloor @@ -996,6 +999,7 @@ zhubx007 zhufl zhurong +zitptan zzxwill Łukasz Jernaś Łukasz Jernaś diff -Nru horizon-19.2.0+git2021072116.b58ac2894/ChangeLog horizon-20.1.0/ChangeLog --- horizon-19.2.0+git2021072116.b58ac2894/ChangeLog 2021-07-21 20:46:27.000000000 +0000 +++ horizon-20.1.0/ChangeLog 2021-09-23 09:32:32.000000000 +0000 @@ -1,16 +1,49 @@ CHANGES ======= +20.1.0 +------ + +* workflow: Do not touch dict during iteration +* Add a release note on Django 3.2 support +* Bump decorator version in lower-constraints.txt +* Support Django 3.2 support (2) +* Preparation for Django 3.2 support +* Preparation for Django 3.0 and 3.1 support (2) +* Support Django 3.2 support (3) +* Support Django 3.2 support (1) +* Support Django 3.0 and 3.1 support (4) +* Support Django 3.0 and 3.1 support (3) +* Support Django 3.0 and 3.1 support (2) +* Add horizontal scrollbar to role dropdown +* Support Django 3.0 and 3.1 support (1) +* Imported Translations from Zanata +* Escape unicode characters when setting logout\_reason cookie +* Changes for tacker-horizon integration tests +* Proper title for Format column +* Show image names at Admin-Instances dashboard +* Add Create QoS operation to Network QoS Panel +* Add POST/Redirect/GET for Domains dashboard + +20.0.0 +------ + +* Clean up the workaround of cinderclient v2->v3 transition * Drop cinder v2 API support +* Enable CSRF token handling for PUT and PATCH reqs +* Fix Unable to use multiattach volume as boot for new server +* Make word wrapping consistent * Imported Translations from Zanata * Imported Translations from Zanata * Imported Translations from Zanata +* Magic Search filter facet removal fix * integration tests: Relax router interface status check * Imported Translations from Zanata * Imported Translations from Zanata * doc: Update our IRC server to OFTC * Imported Translations from Zanata * Default role checker should be case-insensitive +* Added a condition to check whether value is in present in choices for ThemableSelectWidget * Imported Translations from Zanata * Consisent abbreviation of size units * setup.cfg: Replace dashes with underscores @@ -18,6 +51,7 @@ * Imported Translations from Zanata * Imported Translations from Zanata * Imported Translations from Zanata +* Drop nodejs10 job * Setup project-template for nodejs14 jobs * Imported Translations from Zanata * Change with\_data=False for swift\_get\_container @@ -55,6 +89,7 @@ * Handle an attached volume without volume\_image\_metadata * test: Ensure to stop mock when create\_mocks decorator exits * Fix community image handling in launch instance form +* Preparation for Django 3.0 and 3.1 support (1) * Fix typo in horizon/exceptions.py * Imported Translations from Zanata * Fix Material theme to work with any combination of pyScss and MDI icons @@ -67,6 +102,7 @@ * Drop the usage of unicode prefix from unicode strings * Add openstack\_auth to translation-related commands * security\_groups panel: Add neutron policy enforcement +* Update doc8 min version * Imported Translations from Zanata * Use override\_settings decorator consistently * Don't fail integration tests if browser log couldn't be retrieved diff -Nru horizon-19.2.0+git2021072116.b58ac2894/debian/changelog horizon-20.1.0/debian/changelog --- horizon-19.2.0+git2021072116.b58ac2894/debian/changelog 2021-07-21 20:50:25.000000000 +0000 +++ horizon-20.1.0/debian/changelog 2021-10-07 11:21:46.000000000 +0000 @@ -1,3 +1,16 @@ +horizon (4:20.1.0-0ubuntu1) impish; urgency=medium + + * d/watch: Scope to 20.x series. + * New upstream release for OpenStack Xena. + + -- Chris MacNaughton Thu, 07 Oct 2021 11:21:46 +0000 + +horizon (4:20.0.0+git2021091315.420eaa5ba-0ubuntu1) impish; urgency=medium + + * New upstream snapshot for OpenStack Xena. + + -- Corey Bryant Mon, 13 Sep 2021 15:55:38 -0400 + horizon (4:19.2.0+git2021072116.b58ac2894-0ubuntu1) impish; urgency=medium * New upstream snapshot for OpenStack Xena. diff -Nru horizon-19.2.0+git2021072116.b58ac2894/debian/watch horizon-20.1.0/debian/watch --- horizon-19.2.0+git2021072116.b58ac2894/debian/watch 2021-07-21 20:49:57.000000000 +0000 +++ horizon-20.1.0/debian/watch 2021-10-07 10:58:58.000000000 +0000 @@ -1,3 +1,3 @@ version=3 opts="uversionmangle=s/\.([a-zA-Z])/~$1/;s/%7E/~/;s/\.0b/~b/;s/\.0rc/~rc/" \ - https://tarballs.opendev.org/openstack/horizon/ horizon-(19\.\d.*)\.tar\.gz + https://tarballs.opendev.org/openstack/horizon/ horizon-(20\.\d.*)\.tar\.gz diff -Nru horizon-19.2.0+git2021072116.b58ac2894/doc/requirements.txt horizon-20.1.0/doc/requirements.txt --- horizon-19.2.0+git2021072116.b58ac2894/doc/requirements.txt 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/doc/requirements.txt 2021-09-23 09:31:43.000000000 +0000 @@ -8,7 +8,7 @@ openstackdocstheme>=2.2.0 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD -doc8>=0.6.0 # Apache-2.0 +doc8>=0.8.1 # Apache-2.0 # The below is rewquired to build testing module reference mock>=2.0.0 # BSD diff -Nru horizon-19.2.0+git2021072116.b58ac2894/doc/source/install/system-requirements.rst horizon-20.1.0/doc/source/install/system-requirements.rst --- horizon-19.2.0+git2021072116.b58ac2894/doc/source/install/system-requirements.rst 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/doc/source/install/system-requirements.rst 2021-09-23 09:31:43.000000000 +0000 @@ -8,6 +8,8 @@ * Django 2.2 + * Django 3.2 support is experimental as of Xena release. + (Yoga release will use Django 3.2 as the primary Django version.) * Django support policy is documented at :ref:`django_support`. * An accessible `keystone `_ endpoint diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/decorators.py horizon-20.1.0/horizon/decorators.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/decorators.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/decorators.py 2021-09-23 09:31:44.000000000 +0000 @@ -21,13 +21,12 @@ """ import functools -from django.utils.decorators import available_attrs from django.utils.translation import ugettext_lazy as _ def _current_component(view_func, dashboard=None, panel=None): """Sets the currently-active dashboard and/or panel on the request.""" - @functools.wraps(view_func, assigned=available_attrs(view_func)) + @functools.wraps(view_func, assigned=functools.WRAPPER_ASSIGNMENTS) def dec(request, *args, **kwargs): if dashboard: request.horizon['dashboard'] = dashboard @@ -46,7 +45,7 @@ """ from horizon.exceptions import NotAuthenticated - @functools.wraps(view_func, assigned=available_attrs(view_func)) + @functools.wraps(view_func, assigned=functools.WRAPPER_ASSIGNMENTS) def dec(request, *args, **kwargs): if request.user.is_authenticated: return view_func(request, *args, **kwargs) @@ -77,7 +76,7 @@ current_perms = getattr(view_func, '_required_perms', set([])) view_func._required_perms = current_perms | set(required) - @functools.wraps(view_func, assigned=available_attrs(view_func)) + @functools.wraps(view_func, assigned=functools.WRAPPER_ASSIGNMENTS) def dec(request, *args, **kwargs): if request.user.is_authenticated: if request.user.has_perms(view_func._required_perms): @@ -103,7 +102,7 @@ """ from horizon.exceptions import NotAuthorized - @functools.wraps(view_func, assigned=available_attrs(view_func)) + @functools.wraps(view_func, assigned=functools.WRAPPER_ASSIGNMENTS) def dec(request, *args, **kwargs): if not component.can_access({'request': request}): raise NotAuthorized(_("You are not authorized to access %s") diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/forms/fields.py horizon-20.1.0/horizon/forms/fields.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/forms/fields.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/forms/fields.py 2021-09-23 09:31:44.000000000 +0000 @@ -308,6 +308,8 @@ new_choices = [] initial_value = value + # Initially assuming value is not present in choices. + value_in_choices = False for opt_value, opt_label in itertools.chain(self.choices, choices): other_html = self.transform_option_html_attrs(opt_label) @@ -318,15 +320,19 @@ opt_label = self.transform_option_label(opt_label) # If value exists, save off its label for use + # and setting value in choices to True if opt_value == value: initial_value = opt_label + value_in_choices = True if other_html: new_choices.append((opt_value, opt_label, other_html)) else: new_choices.append((opt_value, opt_label)) - if value is None and new_choices: + # if value is None or it is not present in choices then set + # the first value of choices. + if (value is None or not value_in_choices) and new_choices: initial_value = new_choices[0][1] attrs = self.build_attrs(attrs) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/locale/ru/LC_MESSAGES/django.po horizon-20.1.0/horizon/locale/ru/LC_MESSAGES/django.po --- horizon-19.2.0+git2021072116.b58ac2894/horizon/locale/ru/LC_MESSAGES/django.po 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/locale/ru/LC_MESSAGES/django.po 2021-09-23 09:31:44.000000000 +0000 @@ -21,16 +21,17 @@ # Ilya Alekseyev , 2017. #zanata # Ilya Alekseyev , 2018. #zanata # Dmitriy Rabotyagov , 2020. #zanata +# Roman Gorshunov , 2021. #zanata msgid "" msgstr "" "Project-Id-Version: horizon VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2021-05-11 12:54+0000\n" +"POT-Creation-Date: 2021-09-09 10:31+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2020-09-16 04:41+0000\n" -"Last-Translator: Dmitriy Rabotyagov \n" +"PO-Revision-Date: 2021-09-06 04:20+0000\n" +"Last-Translator: Roman Gorshunov \n" "Language: ru\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" @@ -51,6 +52,13 @@ msgstr "%(name)s: %(error)s" #, python-format +msgid "%(size)d B" +msgid_plural "%(size)d B" +msgstr[0] "%(size)d байт" +msgstr[1] "%(size)d байта" +msgstr[2] "%(size)d байт" + +#, python-format msgid "%(usedphrase)s %(used)s (No Limit)" msgstr "%(usedphrase)s %(used)s (без ограничений)" @@ -113,6 +121,9 @@ msgid "-" msgstr "-" +msgid "0 B" +msgstr "0 байт" + #, python-format msgid "A %(resource)s with the name \"%(name)s\" already exists." msgstr "%(resource)s с именем\"%(name)s\" уже существует." diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/static/framework/widgets/magic-search/magic-search.controller.js horizon-20.1.0/horizon/static/framework/widgets/magic-search/magic-search.controller.js --- horizon-19.2.0+git2021072116.b58ac2894/horizon/static/framework/widgets/magic-search/magic-search.controller.js 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/static/framework/widgets/magic-search/magic-search.controller.js 2021-09-23 09:31:44.000000000 +0000 @@ -334,6 +334,10 @@ delete ctrl.textSearch; } else { $scope.$emit(magicSearchEvents.SEARCH_UPDATED, query); + if (angular.isDefined(ctrl.textSearch)) { + // emit text search if text facet remains + emitTextSearch(ctrl.textSearch || ''); + } if (ctrl.currentSearch.length > 0) { // prune facets as needed from menus var newFacet = ctrl.currentSearch[ctrl.currentSearch.length - 1].name; diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/static/framework/widgets/magic-search/magic-search.controller.spec.js horizon-20.1.0/horizon/static/framework/widgets/magic-search/magic-search.controller.spec.js --- horizon-19.2.0+git2021072116.b58ac2894/horizon/static/framework/widgets/magic-search/magic-search.controller.spec.js 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/static/framework/widgets/magic-search/magic-search.controller.spec.js 2021-09-23 09:31:44.000000000 +0000 @@ -509,6 +509,14 @@ expect(scope.strings.prompt).toEqual(''); }); + it("emits textSearch if text facet remains", function() { + spyOn(scope, '$emit'); + ctrl.currentSearch = [{}]; + ctrl.textSearch = 'cat'; + ctrl.removeFacet(0); + expect(scope.$emit).toHaveBeenCalledWith(magicSearchEvents.TEXT_SEARCH, 'cat', undefined); + }); + it("resets state if facet selected", function() { ctrl.currentSearch = [{}]; ctrl.facetSelected = {}; diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/tables/base.py horizon-20.1.0/horizon/tables/base.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/tables/base.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/tables/base.py 2021-09-23 09:31:44.000000000 +0000 @@ -1922,7 +1922,7 @@ def get_columns(self): """Returns this table's columns including auto-generated ones.""" - return self.columns.values() + return list(self.columns.values()) def get_rows(self): """Return the row data for this table broken out by columns.""" diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/templates/auth/_login_form.html horizon-20.1.0/horizon/templates/auth/_login_form.html --- horizon-19.2.0+git2021072116.b58ac2894/horizon/templates/auth/_login_form.html 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/templates/auth/_login_form.html 2021-09-23 09:31:44.000000000 +0000 @@ -52,13 +52,13 @@

{% endif %} - {% if request.COOKIES.logout_reason %} - {% if request.COOKIES.logout_status == "success" %} + {% if logout_reason %} + {% if logout_status == "success" %}
{% else %}
{% endif %} -

{{ request.COOKIES.logout_reason }}

+

{{ logout_reason }}

{% endif %} {% if csrf_failure %} diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/templates/auth/_password_form.html horizon-20.1.0/horizon/templates/auth/_password_form.html --- horizon-19.2.0+git2021072116.b58ac2894/horizon/templates/auth/_password_form.html 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/templates/auth/_password_form.html 2021-09-23 09:31:44.000000000 +0000 @@ -31,13 +31,13 @@
{%endif%}
- {% if request.COOKIES.logout_reason %} - {% if request.COOKIES.logout_status == "success" %} + {% if logout_reason %} + {% if logout_status == "success" %}
{% else %}
{% endif %} -

{{ request.COOKIES.logout_reason }}

+

{{ logout_reason }}

{% endif %} {% include "horizon/common/_form_fields.html" %} diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/test/django_pyscss_fix/__init__.py horizon-20.1.0/horizon/test/django_pyscss_fix/__init__.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/test/django_pyscss_fix/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ horizon-20.1.0/horizon/test/django_pyscss_fix/__init__.py 2021-09-23 09:31:44.000000000 +0000 @@ -0,0 +1,24 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import sys + +import django +import six +import six.moves + +# Temporary workaround for a situation that django-pyscss depends on +# a vendored version of six, django.utils.six which was dropped in Django 3.0. +# TODO(amotoki): Drop the workaround once django-pyscss supports Django 3.0+. +if django.VERSION[0] >= 3: + sys.modules['django.utils.six'] = six + sys.modules['django.utils.six.moves'] = six.moves diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/test/settings.py horizon-20.1.0/horizon/test/settings.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/test/settings.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/test/settings.py 2021-09-23 09:31:44.000000000 +0000 @@ -51,6 +51,7 @@ 'django.contrib.humanize', 'django.contrib.auth', 'django.contrib.contenttypes', + 'horizon.test.django_pyscss_fix', 'django_pyscss', 'compressor', 'horizon', diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/test/unit/tables/test_tables.py horizon-20.1.0/horizon/test/unit/tables/test_tables.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/test/unit/tables/test_tables.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/test/unit/tables/test_tables.py 2021-09-23 09:31:44.000000000 +0000 @@ -425,7 +425,7 @@ # but should not contain the excluded column. # Additionally, auto-generated columns should use the custom # column class specified on the table. - self.assertQuerysetEqual(self.table.columns.values(), + self.assertQuerysetEqual(self.table.get_columns(), ['', '', '', @@ -434,7 +434,7 @@ '', '']) # Actions (these also test ordering) - self.assertQuerysetEqual(self.table.base_actions.values(), + self.assertQuerysetEqual(list(self.table.base_actions.values()), ['', '', '', @@ -472,7 +472,7 @@ self.assertEqual(TEST_DATA, self.table.data) # The column "restricted" is not rendered because of policy expected_columns = [''] - self.assertQuerysetEqual(self.table.columns.values(), expected_columns) + self.assertQuerysetEqual(self.table.get_columns(), expected_columns) @override_settings(POLICY_CHECK_FUNCTION=lambda *args: True) def test_table_column_policy_allowed(self): @@ -480,7 +480,7 @@ self.assertEqual(TEST_DATA, self.table.data) # Policy check returns True so the column "restricted" is rendered expected_columns = ['', ''] - self.assertQuerysetEqual(self.table.columns.values(), expected_columns) + self.assertQuerysetEqual(self.table.get_columns(), expected_columns) def test_table_force_no_multiselect(self): class TempTable(MyTable): @@ -490,7 +490,7 @@ row_actions = (MyAction, MyLinkAction,) multi_select = False self.table = TempTable(self.request, TEST_DATA) - self.assertQuerysetEqual(self.table.columns.values(), + self.assertQuerysetEqual(self.table.get_columns(), ['', '']) @@ -502,7 +502,7 @@ row_actions = (MyAction, MyLinkAction,) actions_column = False self.table = TempTable(self.request, TEST_DATA) - self.assertQuerysetEqual(self.table.columns.values(), + self.assertQuerysetEqual(self.table.get_columns(), ['', '']) @@ -528,7 +528,7 @@ columns = ('id',) table_actions = (MyFilterAction, MyAction, MyBatchAction) self.table = TempTable(self.request, TEST_DATA) - self.assertQuerysetEqual(self.table.columns.values(), + self.assertQuerysetEqual(self.table.get_columns(), ['', '']) @@ -538,7 +538,7 @@ columns = ('id',) row_actions = (MyAction, MyLinkAction,) self.table = TempTable(self.request, TEST_DATA) - self.assertQuerysetEqual(self.table.columns.values(), + self.assertQuerysetEqual(self.table.get_columns(), ['', '']) @@ -552,7 +552,7 @@ row_actions = (MyAction, MyLinkAction,) self.table = TempTable(self.request, TEST_DATA) - self.assertQuerysetEqual(self.table.columns.values(), + self.assertQuerysetEqual(self.table.get_columns(), ['', '', '', diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/test/unit/test_base.py horizon-20.1.0/horizon/test/unit/test_base.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/test/unit/test_base.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/test/unit/test_base.py 2021-09-23 09:31:44.000000000 +0000 @@ -210,11 +210,11 @@ # Test registering a module with a dashboard that defines panels # as a panel group. cats.register(MyPanel) - self.assertQuerysetEqual(cats.get_panel_groups()['other'], + self.assertQuerysetEqual(list(cats.get_panel_groups()['other']), ['']) # Test that panels defined as a tuple still return a PanelGroup dogs = horizon.get_dashboard("dogs") - self.assertQuerysetEqual(dogs.get_panel_groups().values(), + self.assertQuerysetEqual(list(dogs.get_panel_groups().values()), ['']) # Test registering a module with a dashboard that defines panels diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/utils/functions.py horizon-20.1.0/horizon/utils/functions.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/utils/functions.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/utils/functions.py 2021-09-23 09:31:44.000000000 +0000 @@ -43,7 +43,7 @@ # Store the translated string in the cookie lang = translation.get_language_from_request(request) with translation.override(lang): - reason = str(reason) + reason = force_text(reason).encode('unicode_escape').decode('ascii') response.set_cookie('logout_reason', reason, max_age=10) response.set_cookie('logout_status', status, max_age=10) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon/workflows/base.py horizon-20.1.0/horizon/workflows/base.py --- horizon-19.2.0+git2021072116.b58ac2894/horizon/workflows/base.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/horizon/workflows/base.py 2021-09-23 09:31:44.000000000 +0000 @@ -169,7 +169,7 @@ return "<%s: %s>" % (self.__class__.__name__, self.slug) def _populate_choices(self, request, context): - for field_name, bound_field in self.fields.items(): + for field_name, bound_field in list(self.fields.items()): meth = getattr(self, "populate_%s_choices" % field_name, None) if meth is not None and callable(meth): bound_field.choices = meth(request, context) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon.egg-info/pbr.json horizon-20.1.0/horizon.egg-info/pbr.json --- horizon-19.2.0+git2021072116.b58ac2894/horizon.egg-info/pbr.json 2021-07-21 20:46:27.000000000 +0000 +++ horizon-20.1.0/horizon.egg-info/pbr.json 2021-09-23 09:32:33.000000000 +0000 @@ -1 +1 @@ -{"git_version": "b58ac2894", "is_release": false} \ No newline at end of file +{"git_version": "a20c4f6f2", "is_release": true} \ No newline at end of file diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon.egg-info/PKG-INFO horizon-20.1.0/horizon.egg-info/PKG-INFO --- horizon-19.2.0+git2021072116.b58ac2894/horizon.egg-info/PKG-INFO 2021-07-21 20:46:27.000000000 +0000 +++ horizon-20.1.0/horizon.egg-info/PKG-INFO 2021-09-23 09:32:33.000000000 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: horizon -Version: 19.2.0.dev63 +Version: 20.1.0 Summary: OpenStack Dashboard Home-page: https://docs.openstack.org/horizon/latest/ Author: OpenStack diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon.egg-info/requires.txt horizon-20.1.0/horizon.egg-info/requires.txt --- horizon-19.2.0+git2021072116.b58ac2894/horizon.egg-info/requires.txt 2021-07-21 20:46:27.000000000 +0000 +++ horizon-20.1.0/horizon.egg-info/requires.txt 2021-09-23 09:32:33.000000000 +0000 @@ -36,25 +36,26 @@ enmerkar>=0.7.1 futurist>=1.2.0 iso8601>=0.1.11 -keystoneauth1>=3.4.0 +keystoneauth1>=4.3.1 netaddr>=0.7.18 oslo.concurrency>=3.26.0 oslo.config>=5.2.0 -oslo.i18n>=3.15.3 +oslo.i18n>=5.0.1 oslo.policy>=3.2.0 oslo.serialization!=2.19.1,>=2.18.0 oslo.upgradecheck>=0.1.1 -oslo.utils>=3.40.0 +oslo.utils>=4.8.0 osprofiler>=2.3.0 -pbr!=2.1.0,>=2.0.0 +pbr>=5.5.0 pyScss>=1.3.7 pymongo!=3.1,>=3.0.2 -python-cinderclient>=5.0.0 +python-cinderclient>=8.0.0 python-glanceclient>=2.8.0 python-keystoneclient>=3.22.0 python-neutronclient>=6.7.0 python-novaclient>=9.1.0 python-swiftclient>=3.2.0 pytz>=2013.6 -requests>=2.14.2 +requests>=2.25.1 semantic-version>=2.3.1 +six>=1.16.0 diff -Nru horizon-19.2.0+git2021072116.b58ac2894/horizon.egg-info/SOURCES.txt horizon-20.1.0/horizon.egg-info/SOURCES.txt --- horizon-19.2.0+git2021072116.b58ac2894/horizon.egg-info/SOURCES.txt 2021-07-21 20:46:28.000000000 +0000 +++ horizon-20.1.0/horizon.egg-info/SOURCES.txt 2021-09-23 09:32:35.000000000 +0000 @@ -702,6 +702,7 @@ horizon/test/customization/__init__.py horizon/test/customization/cust_test1.py horizon/test/customization/cust_test2.py +horizon/test/django_pyscss_fix/__init__.py horizon/test/dummy_auth/__init__.py horizon/test/dummy_auth/backend.py horizon/test/jasmine/__init__.py @@ -2249,8 +2250,12 @@ openstack_dashboard/static/app/core/network_qos/qos.service.spec.js openstack_dashboard/static/app/core/network_qos/actions/actions.module.js openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js +openstack_dashboard/static/app/core/network_qos/actions/create.action.service.js +openstack_dashboard/static/app/core/network_qos/actions/create.action.service.spec.js openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.js openstack_dashboard/static/app/core/network_qos/actions/delete.action.service.spec.js +openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.js +openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.spec.js openstack_dashboard/static/app/core/network_qos/details/details.module.js openstack_dashboard/static/app/core/network_qos/details/details.module.spec.js openstack_dashboard/static/app/core/network_qos/details/drawer.html @@ -2900,6 +2905,7 @@ releasenotes/notes/django-2.0-b37c6e91d20519fa.yaml releasenotes/notes/django-2.2-2aff36c491fb7b95.yaml releasenotes/notes/django-version-queens-b7785b96ecbceaf0.yaml +releasenotes/notes/django32-support-78232a850d04842c.yaml releasenotes/notes/domains-0581aa42773d5f41.yaml releasenotes/notes/drop-LBaaS-v1-dashboard-d767b0bde5274af5.yaml releasenotes/notes/drop-action-strings-attributes-64f0cb0323f629ee.yaml @@ -2959,6 +2965,7 @@ releasenotes/notes/multi-attached-volume-support-3d32cde6f296cdd9.yaml releasenotes/notes/network-type-geneve-71eed4104699754e.yaml releasenotes/notes/network-type-midonet-6c78bdfe1e3186a0.yaml +releasenotes/notes/network_qos-ee068d073e86de76.yaml releasenotes/notes/neutron-default-quotas-ddd237af2935fde3.yaml releasenotes/notes/ngdetail-reload-e711a77b2d07191a.yaml releasenotes/notes/openrc-clouds-yaml-link-f1642b77e25f08ba.yaml diff -Nru horizon-19.2.0+git2021072116.b58ac2894/lower-constraints.txt horizon-20.1.0/lower-constraints.txt --- horizon-19.2.0+git2021072116.b58ac2894/lower-constraints.txt 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/lower-constraints.txt 2021-09-23 09:31:44.000000000 +0000 @@ -13,7 +13,7 @@ coverage==4.0 cryptography==3.0 debtcollector==1.2.0 -decorator==3.4.0 +decorator==4.4.2 deprecation==1.0 Django==2.2 django-appconf==1.0.2 @@ -39,7 +39,7 @@ jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 -keystoneauth1==3.4.0 +keystoneauth1==4.3.1 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.0 @@ -57,7 +57,7 @@ oslo.concurrency==3.26.0 oslo.config==5.2.0 oslo.context==2.22.0 -oslo.i18n==3.15.3 +oslo.i18n==5.0.1 oslo.log==3.36.0 oslo.messaging==5.29.0 oslo.middleware==3.31.0 @@ -65,11 +65,11 @@ oslo.serialization==2.18.0 oslo.service==1.24.0 oslo.upgradecheck==0.1.1 -oslo.utils==3.40.0 +oslo.utils==4.8.0 osprofiler==2.3.0 Paste==2.0.2 PasteDeploy==1.5.0 -pbr==2.0.0 +pbr==5.5.0 pep8==1.5.7 pika==0.10.0 pika-pool==0.1.3 @@ -87,7 +87,7 @@ pytest==5.3.5 pytest-django==3.8.0 pytest-html==2.0.1 -python-cinderclient==5.0.0 +python-cinderclient==8.0.0 python-dateutil==2.8.1 python-glanceclient==2.8.0 python-keystoneclient==3.22.0 @@ -101,7 +101,7 @@ rcssmin==1.0.6 reno==3.1.0 repoze.lru==0.7 -requests==2.14.2 +requests==2.25.1 requestsexceptions==1.2.0 restructuredtext-lint==1.1.1 rfc3986==0.3.1 @@ -110,10 +110,10 @@ selenium==2.50.1 semantic-version==2.3.1 simplejson==3.5.1 -six==1.12.0 +six==1.16.0 snowballstemmer==1.2.1 statsd==3.2.1 -stevedore==1.20.0 +stevedore==3.3.0 tenacity==3.2.1 termcolor==1.1.0 testscenarios==0.4 diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_auth/locale/ru/LC_MESSAGES/django.po horizon-20.1.0/openstack_auth/locale/ru/LC_MESSAGES/django.po --- horizon-19.2.0+git2021072116.b58ac2894/openstack_auth/locale/ru/LC_MESSAGES/django.po 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_auth/locale/ru/LC_MESSAGES/django.po 2021-09-23 09:31:44.000000000 +0000 @@ -2,16 +2,17 @@ # Ilya Alekseyev , 2018. #zanata # Roman Gorshunov , 2019. #zanata # Dmitriy Rabotyagov , 2020. #zanata +# Roman Gorshunov , 2021. #zanata msgid "" msgstr "" "Project-Id-Version: horizon VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2020-01-08 17:20+0000\n" +"POT-Creation-Date: 2021-09-09 10:31+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2020-01-08 12:42+0000\n" -"Last-Translator: Dmitriy Rabotyagov \n" +"PO-Revision-Date: 2021-09-06 04:21+0000\n" +"Last-Translator: Roman Gorshunov \n" "Language-Team: Russian\n" "Language: ru\n" "X-Generator: Zanata 4.3.3\n" @@ -27,6 +28,11 @@ msgid "Confirm password" msgstr "Подтвердите пароль" +msgid "Cookies may be turned off. Make sure cookies are enabled and try again." +msgstr "" +"Механизм Cookies может быть выключен. Убедитесь, что cookies разрешены и " +"попробуйте снова." + msgid "Could not find service provider ID on keystone." msgstr "Не удалось найти ID поставщика служб в Keystone." diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_auth/views.py horizon-20.1.0/openstack_auth/views.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_auth/views.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_auth/views.py 2021-09-23 09:31:44.000000000 +0000 @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. import datetime +import functools import logging from django.conf import settings @@ -22,7 +23,6 @@ from django.middleware import csrf from django import shortcuts from django.urls import reverse -from django.utils import functional from django.utils import http from django.utils.translation import ugettext_lazy as _ from django.views.decorators.cache import never_cache @@ -66,6 +66,11 @@ return reason +def set_logout_reason(res, msg): + msg = msg.encode('unicode_escape').decode('ascii') + res.set_cookie('logout_reason', msg, max_age=10) + + # TODO(stephenfin): Migrate to CBV @sensitive_post_parameters() @csrf_protect @@ -116,12 +121,15 @@ initial.update({'region': requested_region}) if request.method == "POST": - form = functional.curry(forms.Login) + form = functools.partial(forms.Login) else: - form = functional.curry(forms.Login, initial=initial) + form = functools.partial(forms.Login, initial=initial) choices = settings.WEBSSO_CHOICES reason = get_csrf_reason(request.GET.get('csrf_failure')) + logout_reason = request.COOKIES.get( + 'logout_reason', '').encode('ascii').decode('unicode_escape') + logout_status = request.COOKIES.get('logout_status') extra_context = { 'redirect_field_name': auth.REDIRECT_FIELD_NAME, 'csrf_failure': reason, @@ -131,6 +139,8 @@ 'single_value': '', 'label': '', }, + 'logout_reason': logout_reason, + 'logout_status': logout_status, } if request.is_ajax(): @@ -150,7 +160,7 @@ res = django_http.HttpResponseRedirect( reverse('password', args=[exc.user_id])) msg = _("Your password has expired. Please set a new password.") - res.set_cookie('logout_reason', msg, max_age=10) + set_logout_reason(res, msg) # Save the region in the cookie, this is used as the default # selected region next time the Login form loads. @@ -201,7 +211,7 @@ else: msg = 'Login failed: %s' % exc res = django_http.HttpResponseRedirect(settings.LOGIN_URL) - res.set_cookie('logout_reason', msg, max_age=10) + set_logout_reason(res, msg) return res auth_user.set_session_from_user(request, request.user) @@ -373,7 +383,7 @@ except exceptions.KeystoneAuthException as exc: msg = 'Keystone provider switch failed: %s' % exc res = django_http.HttpResponseRedirect(settings.LOGIN_URL) - res.set_cookie('logout_reason', msg, max_age=10) + set_logout_reason(res, msg) return res auth.login(request, request.user) auth_user.set_session_from_user(request, request.user) @@ -403,5 +413,5 @@ # We have no session here, so regular messages don't work. msg = _('Password changed. Please log in to continue.') res = django_http.HttpResponseRedirect(self.success_url) - res.set_cookie('logout_reason', msg, max_age=10) + set_logout_reason(res, msg) return res diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/glance.py horizon-20.1.0/openstack_dashboard/api/glance.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/glance.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/api/glance.py 2021-09-23 09:31:44.000000000 +0000 @@ -46,18 +46,6 @@ VERSIONS.load_supported_version(2, {"client": client, "version": 2}) -# TODO(e0ne): remove this workaround once glanceclient will raise -# RequestURITooLong exception - -# NOTE(e0ne): set MAX_URI_LEN to 8KB like Neutron does -MAX_URI_LEN = 8192 - -URI_FILTER_PREFIX = "/v2/images?id=in:" -# NOTE(e0ne): 36 is a lengght of UUID, we need tp have '+' for sapparator. -# Decreasing value by 1 to make sure it could be send to a server -MAX_IMGAGES_PER_REQUEST = \ - (MAX_URI_LEN - len(URI_FILTER_PREFIX)) // (36 + 1) - 1 - class Image(base.APIResourceWrapper): _attrs = {"architecture", "container_format", "disk_format", "created_at", @@ -197,19 +185,6 @@ @profiler.trace -def image_list_detailed_by_ids(request, ids=None): - images = [] - if not ids: - return images - for i in range(0, len(ids), MAX_IMGAGES_PER_REQUEST): - ids_to_filter = ids[i:i + MAX_IMGAGES_PER_REQUEST] - filters = {'id': 'in:' + ','.join(ids_to_filter)} - images.extend(image_list_detailed(request, filters=filters)[0]) - - return images - - -@profiler.trace def image_list_detailed(request, marker=None, sort_dir='desc', sort_key='created_at', filters=None, paginate=False, reversed_order=False, **kwargs): diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/microversions.py horizon-20.1.0/openstack_dashboard/api/microversions.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/microversions.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/api/microversions.py 2021-09-23 09:31:44.000000000 +0000 @@ -29,12 +29,12 @@ MICROVERSION_FEATURES = { "nova": { "locked_attribute": ["2.9", "2.42"], - "instance_description": ["2.19", "2.42"], + "instance_description": ["2.19", "2.60"], "remote_console_mks": ["2.8", "2.53"], "servergroup_soft_policies": ["2.15", "2.60"], "servergroup_user_info": ["2.13", "2.60"], "multiattach": ["2.60"], - "auto_allocated_network": ["2.37", "2.42"], + "auto_allocated_network": ["2.37", "2.60"], "key_types": ["2.2", "2.9"], "key_type_list": ["2.9"], }, diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/nova.py horizon-20.1.0/openstack_dashboard/api/nova.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/nova.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/api/nova.py 2021-09-23 09:31:44.000000000 +0000 @@ -411,7 +411,8 @@ availability_zone=None, instance_count=1, admin_pass=None, disk_config=None, config_drive=None, meta=None, scheduler_hints=None, description=None): - microversion = get_microversion(request, ("instance_description", + microversion = get_microversion(request, ("multiattach", + "instance_description", "auto_allocated_network")) nova_client = _nova.novaclient(request, version=microversion) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/rest/neutron.py horizon-20.1.0/openstack_dashboard/api/rest/neutron.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/rest/neutron.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/api/rest/neutron.py 2021-09-23 09:31:44.000000000 +0000 @@ -301,6 +301,20 @@ tenant_id=request.user.project_id) return {'items': [p.to_dict() for p in result]} + @rest_utils.ajax(data_required=True) + def post(self, request): + """Create a Network QoS policy. + + Create a qos policy using parameters supplied in the POST + application/json object. The "name" (string) parameter is required. + This method returns the new qos policy object on success. + """ + qospolicy = api.neutron.policy_create(request, **request.DATA) + return rest_utils.CreatedResponse( + '/api/neutron/qos_policies/%s' % qospolicy.id, + qospolicy.to_dict() + ) + @urls.register class QoSPolicy(generic.View): diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/rest/utils.py horizon-20.1.0/openstack_dashboard/api/rest/utils.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/api/rest/utils.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/api/rest/utils.py 2021-09-23 09:31:44.000000000 +0000 @@ -17,7 +17,6 @@ from django.conf import settings from django import http -from django.utils import decorators from oslo_serialization import jsonutils @@ -104,7 +103,7 @@ def decorator(function, authenticated=authenticated, data_required=data_required): @functools.wraps(function, - assigned=decorators.available_attrs(function)) + assigned=functools.WRAPPER_ASSIGNMENTS) def _wrapped(self, request, *args, **kw): if authenticated and not request.user.is_authenticated: return JSONResponse('not logged in', 401) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/aggregates/tests.py horizon-20.1.0/openstack_dashboard/dashboards/admin/aggregates/tests.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/aggregates/tests.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/admin/aggregates/tests.py 2021-09-23 09:31:44.000000000 +0000 @@ -104,7 +104,7 @@ workflow_data['name'] = '' workflow_data['availability_zone'] = '' self._test_generic_create_aggregate(workflow_data, aggregate, (), 1, - 'This field is required') + 'This field is required.') def test_create_aggregate_fails_missing_fields_existing_aggregates(self): aggregate = self.aggregates.first() @@ -115,7 +115,7 @@ self._test_generic_create_aggregate(workflow_data, aggregate, existing_aggregates, 1, - 'This field is required') + 'This field is required.') def test_create_aggregate_fails_duplicated_name(self): aggregate = self.aggregates.first() @@ -254,7 +254,7 @@ 'availability_zone': aggregate.availability_zone} self._test_generic_update_aggregate(form_data, aggregate, 1, - 'This field is required') + 'This field is required.') def test_update_aggregate_fails_missing_az_field(self): aggregate = self.aggregates.first() diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/info/tables.py horizon-20.1.0/openstack_dashboard/dashboards/admin/info/tables.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/info/tables.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/admin/info/tables.py 2021-09-23 09:31:44.000000000 +0000 @@ -103,7 +103,7 @@ class NovaServicesTable(tables.DataTable): binary = tables.Column("binary", verbose_name=_('Name')) - host = tables.Column('host', verbose_name=_('Host')) + host = tables.WrappingColumn('host', verbose_name=_('Host')) zone = tables.Column('zone', verbose_name=_('Zone')) status = tables.Column(get_agent_status, verbose_name=_('Status')) state = tables.Column('state', verbose_name=_('State'), @@ -127,7 +127,7 @@ class CinderServicesTable(tables.DataTable): binary = tables.Column("binary", verbose_name=_('Name')) - host = tables.Column('host', verbose_name=_('Host')) + host = tables.WrappingColumn('host', verbose_name=_('Host')) zone = tables.Column('zone', verbose_name=_('Zone')) status = tables.Column(get_agent_status, verbose_name=_('Status')) state = tables.Column('state', verbose_name=_('State'), @@ -202,7 +202,7 @@ class NetworkAgentsTable(tables.DataTable): agent_type = tables.Column('agent_type', verbose_name=_('Type')) binary = tables.Column("binary", verbose_name=_('Name')) - host = tables.Column('host', verbose_name=_('Host')) + host = tables.WrappingColumn('host', verbose_name=_('Host')) zone = tables.Column(get_network_agent_zone, verbose_name=_('Zone')) status = tables.Column(get_network_agent_status, verbose_name=_('Status')) state = tables.Column(get_network_agent_state, verbose_name=_('State')) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/instances/tables.py horizon-20.1.0/openstack_dashboard/dashboards/admin/instances/tables.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/instances/tables.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/admin/instances/tables.py 2021-09-23 09:31:44.000000000 +0000 @@ -147,14 +147,13 @@ # techniques isn't practical. It can be added back in when we have names # returned in a practical manner by the API. # user = tables.Column("user_id", verbose_name=_("User")) - host = tables.Column("OS-EXT-SRV-ATTR:host", - verbose_name=_("Host"), - classes=('nowrap-col',)) + host = tables.WrappingColumn("OS-EXT-SRV-ATTR:host", + verbose_name=_("Host")) name = tables.WrappingColumn("name", link=get_server_detail_link, verbose_name=_("Name")) - image_name = tables.Column("image_name", - verbose_name=_("Image Name")) + image_name = tables.WrappingColumn("image_name", + verbose_name=_("Image Name")) ip = tables.Column(project_tables.get_ips, verbose_name=_("IP Address"), attrs={'data-type': "ip"}) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/instances/tests.py horizon-20.1.0/openstack_dashboard/dashboards/admin/instances/tests.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/instances/tests.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/admin/instances/tests.py 2021-09-23 09:31:44.000000000 +0000 @@ -29,20 +29,32 @@ class InstanceViewTest(test.BaseAdminViewTests): + + def _mock_image_list_detailed_side_effect(self, *args, **kwargs): + images = self.images.list() + if 'filters' in kwargs: + return [[image for image in images if + image.visibility == 'community']] + else: + return [[image for image in images if + image.visibility != 'community']] + @test.create_mocks({ api.nova: ['flavor_list', 'server_list_paged'], api.keystone: ['tenant_list'], - api.glance: ['image_list_detailed_by_ids'], + api.glance: ['image_list_detailed'], + api.cinder: ['volume_list'] }) def test_index(self): servers = self.servers.list() # TODO(vmarkov) instances_img_ids should be in test_data - instances_img_ids = [instance.image.get('id') for instance in - servers if isinstance(instance.image, dict)] self.mock_tenant_list.return_value = [self.tenants.list(), False] - self.mock_image_list_detailed_by_ids.return_value = self.images.list() + + self.mock_image_list_detailed.side_effect =\ + self._mock_image_list_detailed_side_effect self.mock_flavor_list.return_value = self.flavors.list() self.mock_server_list_paged.return_value = [servers, False, False] + self.mock_volume_list.return_value = self.cinder_volumes.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, INDEX_TEMPLATE) @@ -50,8 +62,7 @@ self.assertCountEqual(instances, servers) self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) - self.mock_image_list_detailed_by_ids.assert_called_once_with( - test.IsHttpRequest(), instances_img_ids) + self.assertEqual(self.mock_image_list_detailed.call_count, 4) self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) search_opts = {'marker': None, 'paginate': True, 'all_tenants': True} self.mock_server_list_paged.assert_called_once_with( @@ -62,13 +73,12 @@ @test.create_mocks({ api.nova: ['flavor_list', 'flavor_get', 'server_list_paged'], api.keystone: ['tenant_list'], - api.glance: ['image_list_detailed_by_ids'], + api.glance: ['image_list_detailed'], + api.cinder: ['volume_list'] }) def test_index_flavor_list_exception(self): servers = self.servers.list() flavors = self.flavors.list() - instances_img_ids = [instance.image.get('id') for instance in - servers if hasattr(instance, 'image')] full_flavors = OrderedDict([(f.id, f) for f in flavors]) self.mock_server_list_paged.return_value = [servers, False, False] self.mock_flavor_list.side_effect = self.exceptions.nova @@ -78,8 +88,9 @@ return full_flavors[id] self.mock_flavor_get.side_effect = _get_full_flavor - self.mock_image_list_detailed_by_ids.return_value = self.images.list() - + self.mock_image_list_detailed.side_effect =\ + self._mock_image_list_detailed_side_effect + self.mock_volume_list.return_value = self.cinder_volumes.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, INDEX_TEMPLATE) @@ -96,24 +107,24 @@ self.mock_flavor_get.assert_has_calls( [mock.call(test.IsHttpRequest(), s.flavor['id']) for s in servers]) self.assertEqual(len(servers), self.mock_flavor_get.call_count) - self.mock_image_list_detailed_by_ids.assert_called_once_with( - test.IsHttpRequest(), instances_img_ids) + self.assertEqual(self.mock_image_list_detailed.call_count, 4) @test.create_mocks({ api.nova: ['flavor_list', 'flavor_get', 'server_list_paged'], api.keystone: ['tenant_list'], - api.glance: ['image_list_detailed_by_ids'], + api.glance: ['image_list_detailed'], + api.cinder: ['volume_list'] }) def test_index_flavor_get_exception(self): servers = self.servers.list() - instances_img_ids = [instance.image.get('id') for instance in - servers if hasattr(instance, 'image')] # UUIDs generated using indexes are unlikely to match # any of existing flavor ids and are guaranteed to be deterministic. for i, server in enumerate(servers): server.flavor['id'] = str(uuid.UUID(int=i)) - self.mock_image_list_detailed_by_ids.return_value = self.images.list() + self.mock_image_list_detailed.side_effect =\ + self._mock_image_list_detailed_side_effect + self.mock_volume_list.return_value = self.cinder_volumes.list() self.mock_flavor_list.return_value = self.flavors.list() self.mock_server_list_paged.return_value = [servers, False, False] self.mock_tenant_list.return_value = [self.tenants.list(), False] @@ -128,8 +139,7 @@ self.assertMessageCount(res, error=1) self.assertCountEqual(instances, servers) - self.mock_image_list_detailed_by_ids.assert_called_once_with( - test.IsHttpRequest(), instances_img_ids) + self.assertEqual(self.mock_image_list_detailed.call_count, 4) self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) search_opts = {'marker': None, 'paginate': True, 'all_tenants': True} self.mock_server_list_paged.assert_called_once_with( @@ -144,13 +154,16 @@ @test.create_mocks({ api.nova: ['server_list_paged', 'flavor_list'], api.keystone: ['tenant_list'], - api.glance: ['image_list_detailed_by_ids'], + api.glance: ['image_list_detailed'], + api.cinder: ['volume_list'] }) def test_index_server_list_exception(self): self.mock_server_list_paged.side_effect = self.exceptions.nova self.mock_flavor_list.return_value = self.flavors.list() self.mock_tenant_list.return_value = [self.tenants.list(), False] - self.mock_image_list_detailed_by_ids.return_value = self.images.list() + self.mock_image_list_detailed.side_effect =\ + self._mock_image_list_detailed_side_effect + self.mock_volume_list.return_value = self.cinder_volumes.list() res = self.client.get(INDEX_URL) self.assertTemplateUsed(res, INDEX_TEMPLATE) @@ -162,8 +175,7 @@ sort_dir='desc', search_opts=search_opts) self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) - self.mock_image_list_detailed_by_ids.assert_called_once_with( - test.IsHttpRequest(), []) + self.assertEqual(self.mock_image_list_detailed.call_count, 4) self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) @test.create_mocks({api.nova: ['server_get', 'flavor_get'], @@ -206,14 +218,14 @@ @test.create_mocks({ api.nova: ['flavor_list', 'server_list_paged'], api.keystone: ['tenant_list'], - api.glance: ['image_list_detailed_by_ids'], + api.glance: ['image_list_detailed'], + api.cinder: ['volume_list'] }) def test_index_options_before_migrate(self): - servers = self.servers.list() - instances_img_ids = [instance.image.get('id') for instance in - servers if hasattr(instance, 'image')] self.mock_tenant_list.return_value = [self.tenants.list(), False] - self.mock_image_list_detailed_by_ids.return_value = self.images.list() + self.mock_image_list_detailed.side_effect =\ + self._mock_image_list_detailed_side_effect + self.mock_volume_list.return_value = self.cinder_volumes.list() self.mock_flavor_list.return_value = self.flavors.list() self.mock_server_list_paged.return_value = [ self.servers.list(), False, False] @@ -223,8 +235,7 @@ self.assertNotContains(res, "instances__revert") self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) - self.mock_image_list_detailed_by_ids.assert_called_once_with( - test.IsHttpRequest(), instances_img_ids) + self.assertEqual(self.mock_image_list_detailed.call_count, 4) self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) search_opts = {'marker': None, 'paginate': True, 'all_tenants': True} self.mock_server_list_paged.assert_called_once_with( @@ -235,7 +246,8 @@ @test.create_mocks({ api.nova: ['flavor_list', 'server_list_paged'], api.keystone: ['tenant_list'], - api.glance: ['image_list_detailed_by_ids'], + api.glance: ['image_list_detailed'], + api.cinder: ['volume_list'] }) def test_index_options_after_migrate(self): servers = self.servers.list() @@ -243,10 +255,10 @@ server1.status = "VERIFY_RESIZE" server2 = servers[2] server2.status = "VERIFY_RESIZE" - instances_img_ids = [instance.image.get('id') for instance in - servers if hasattr(instance, 'image')] self.mock_tenant_list.return_value = [self.tenants.list(), False] - self.mock_image_list_detailed_by_ids.return_value = self.images.list() + self.mock_image_list_detailed.side_effect =\ + self._mock_image_list_detailed_side_effect + self.mock_volume_list.return_value = self.cinder_volumes.list() self.mock_flavor_list.return_value = self.flavors.list() self.mock_server_list_paged.return_value = [servers, False, False] @@ -256,8 +268,7 @@ self.assertNotContains(res, "instances__migrate") self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) - self.mock_image_list_detailed_by_ids.assert_called_once_with( - test.IsHttpRequest(), instances_img_ids) + self.assertEqual(self.mock_image_list_detailed.call_count, 4) self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) search_opts = {'marker': None, 'paginate': True, 'all_tenants': True} self.mock_server_list_paged.assert_called_once_with( @@ -453,7 +464,8 @@ 'flavor_get', 'server_list_paged'], api.keystone: ['tenant_list'], - api.glance: ['image_list_detailed_by_ids'], + api.glance: ['image_list_detailed'], + api.cinder: ['volume_list'] }) def _test_servers_paginate_do(self, marker, @@ -462,7 +474,6 @@ has_prev): flavors = self.flavors.list() tenants = self.tenants.list() - images = self.images.list() # UUID indices are unique and are guaranteed being deterministic. for i, server in enumerate(servers): server.flavor['id'] = str(uuid.UUID(int=i)) @@ -470,7 +481,9 @@ self.mock_server_list_paged.return_value = [ servers, has_more, has_prev] self.mock_flavor_list.return_value = flavors - self.mock_image_list_detailed_by_ids.return_value = images + self.mock_image_list_detailed.side_effect =\ + self._mock_image_list_detailed_side_effect + self.mock_volume_list.return_value = self.cinder_volumes.list() self.mock_tenant_list.return_value = [tenants, False] self.mock_flavor_get.side_effect = self.exceptions.nova @@ -483,9 +496,7 @@ self.assertEqual(res.status_code, 200) self.mock_tenant_list.assert_called_once_with(test.IsHttpRequest()) - self.mock_image_list_detailed_by_ids.assert_called_once_with( - test.IsHttpRequest(), - [server.image.id for server in servers]) + self.assertEqual(self.mock_image_list_detailed.call_count, 4) self.mock_flavor_list.assert_called_once_with(test.IsHttpRequest()) search_opts = {'marker': marker, 'paginate': True, 'all_tenants': True} self.mock_server_list_paged.assert_called_once_with( diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/instances/views.py horizon-20.1.0/openstack_dashboard/dashboards/admin/instances/views.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/instances/views.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/admin/instances/views.py 2021-09-23 09:31:44.000000000 +0000 @@ -93,16 +93,24 @@ exceptions.handle(self.request, msg) return {} - def _get_images(self, instances=()): + def _get_images(self): # Gather our images to correlate our instances to them try: - # NOTE(aarefiev): request images, instances was booted from. - img_ids = (instance.image.get('id') for instance in - instances if isinstance(instance.image, dict)) - real_img_ids = list(filter(None, img_ids)) - images = api.glance.image_list_detailed_by_ids( - self.request, real_img_ids) - image_map = dict((image.id, image) for image in images) + # Community images have to be retrieved separately and merged, + # because their visibility has to be explicitly defined in the + # API call and the Glance API currently does not support filtering + # by multiple values in the visibility field. + # TODO(gabriel): Handle pagination. + images = api.glance.image_list_detailed(self.request)[0] + community_images = api.glance.image_list_detailed( + self.request, filters={'visibility': 'community'})[0] + image_map = { + image.id: image for image in images + } + # Images have to be filtered by their uuids; some users + # have default access to certain community images. + for image in community_images: + image_map.setdefault(image.id, image) return image_map except Exception: exceptions.handle(self.request, ignore=True) @@ -137,6 +145,15 @@ _('Unable to retrieve instance list.')) return instances + def _get_volumes(self): + # Gather our volumes to get their image metadata for instance + try: + volumes = api.cinder.volume_list(self.request) + return dict((str(volume.id), volume) for volume in volumes) + except Exception: + exceptions.handle(self.request, ignore=True) + return {} + def get_data(self): marker, sort_dir = self._get_marker() default_search_opts = {'marker': marker, @@ -158,9 +175,11 @@ self._needs_filter_first = False results = futurist_utils.call_functions_parallel( + self._get_images, + self._get_volumes, self._get_flavors, self._get_tenants) - flavor_dict, tenant_dict = results + image_dict, volume_dict, flavor_dict, tenant_dict = results non_api_filter_info = [ ('project', 'tenant_id', tenant_dict.values()), @@ -181,10 +200,11 @@ instances = self._get_instances(search_opts, sort_dir) if not filter_by_image_name: - image_dict = self._get_images(tuple(instances)) + image_dict = self._get_images() # Loop through instances to get image, flavor and tenant info. for inst in instances: + self._populate_image_info(inst, image_dict, volume_dict) if hasattr(inst, 'image') and isinstance(inst.image, dict): image_id = inst.image.get('id') if image_id in image_dict: @@ -211,6 +231,49 @@ inst.tenant_name = getattr(tenant, "name", None) return instances + def _populate_image_info(self, instance, image_dict, volume_dict): + if not hasattr(instance, 'image'): + return + # Instance from image returns dict + if isinstance(instance.image, dict): + image_id = instance.image.get('id') + if image_id in image_dict: + instance.image = image_dict[image_id] + # In case image not found in image_dict, set name to empty + # to avoid fallback API call to Glance in api/nova.py + # until the call is deprecated in api itself + else: + instance.image['name'] = _("-") + # Otherwise trying to get image from volume metadata + else: + instance_volumes = [ + attachment + for volume in volume_dict.values() + for attachment in volume.attachments + if attachment['server_id'] == instance.id + ] + # While instance from volume is being created, + # it does not have volumes + if not instance_volumes: + return + # Sorting attached volumes by device name (eg '/dev/sda') + instance_volumes.sort(key=lambda attach: attach['device']) + # Getting volume object, which is as attached + # as the first device + boot_volume = volume_dict[instance_volumes[0]['id']] + # There is a case where volume_image_metadata contains + # only fields other than 'image_id' (See bug 1834747), + # so we try to populate image information only when it is found. + volume_metadata = getattr(boot_volume, "volume_image_metadata", {}) + image_id = volume_metadata.get('image_id') + if image_id: + try: + instance.image = image_dict[image_id].to_dict() + except KeyError: + # KeyError occurs when volume was created from image and + # then this image is deleted. + pass + class LiveMigrateView(forms.ModalFormView): form_class = project_forms.LiveMigrateForm diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/networks/agents/tables.py horizon-20.1.0/openstack_dashboard/dashboards/admin/networks/agents/tables.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/networks/agents/tables.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/admin/networks/agents/tables.py 2021-09-23 09:31:44.000000000 +0000 @@ -83,7 +83,7 @@ class DHCPAgentsTable(tables.DataTable): id = tables.Column('id', verbose_name=_('ID'), hidden=True) - host = tables.Column('host', verbose_name=_('Host')) + host = tables.WrappingColumn('host', verbose_name=_('Host')) status = tables.Column(get_agent_status, verbose_name=_('Status')) state = tables.Column(get_agent_state, verbose_name=_('Admin State')) heartbeat_timestamp = tables.Column('heartbeat_timestamp', diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/snapshots/tables.py horizon-20.1.0/openstack_dashboard/dashboards/admin/snapshots/tables.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/snapshots/tables.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/admin/snapshots/tables.py 2021-09-23 09:31:44.000000000 +0000 @@ -59,7 +59,7 @@ volume_name = snapshots_tables.SnapshotVolumeNameColumn( "name", verbose_name=_("Volume Name"), link="horizon:admin:volumes:detail") - host = tables.Column("host_name", verbose_name=_("Host")) + host = tables.WrappingColumn("host_name", verbose_name=_("Host")) tenant = tables.Column("tenant_name", verbose_name=_("Project")) group_snapshot = snapshots_tables.GroupSnapshotNameColumn( "name", diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/volumes/tables.py horizon-20.1.0/openstack_dashboard/dashboards/admin/volumes/tables.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/admin/volumes/tables.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/admin/volumes/tables.py 2021-09-23 09:31:44.000000000 +0000 @@ -102,7 +102,8 @@ link="horizon:admin:volumes:detail") attachments = AttachmentColumn("attachments", verbose_name=_("Attached To")) - host = tables.Column("os-vol-host-attr:host", verbose_name=_("Host")) + host = tables.WrappingColumn("os-vol-host-attr:host", + verbose_name=_("Host")) tenant = tables.Column(lambda obj: getattr(obj, 'tenant_name', None), verbose_name=_("Project")) group = volumes_tables.GroupNameColumn( diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/identity/domains/tables.py horizon-20.1.0/openstack_dashboard/dashboards/identity/domains/tables.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/identity/domains/tables.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/identity/domains/tables.py 2021-09-23 09:31:44.000000000 +0000 @@ -15,6 +15,7 @@ import logging from django.conf import settings +from django import shortcuts from django.template import defaultfilters as filters from django.urls import reverse from django.utils.http import urlencode @@ -246,9 +247,11 @@ messages.success(request, _('Domain Context updated to Domain %s.') % domain.name) + return shortcuts.redirect(request.get_full_path()) except Exception: messages.error(request, _('Unable to set Domain Context.')) + return shortcuts.redirect(request.get_full_path()) class UnsetDomainContext(tables.Action): @@ -268,6 +271,7 @@ request.session.pop("domain_context") request.session.pop("domain_context_name") messages.success(request, _('Domain Context cleared.')) + return shortcuts.redirect(request.get_full_path()) class DomainsTable(tables.DataTable): diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/identity/domains/tests.py horizon-20.1.0/openstack_dashboard/dashboards/identity/domains/tests.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/identity/domains/tests.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/identity/domains/tests.py 2021-09-23 09:31:44.000000000 +0000 @@ -153,15 +153,21 @@ self.mock_domain_list.return_value = self.domains.list() formData = {'action': 'domains__set_domain_context__%s' % domain.id} - res = self.client.post(DOMAINS_INDEX_URL, formData) + res = self.client.post(DOMAINS_INDEX_URL, formData, follow=True) + self.assertRedirects(res, DOMAINS_INDEX_URL, status_code=302, + target_status_code=200, + fetch_redirect_response=True) self.assertTemplateUsed(res, constants.DOMAINS_INDEX_VIEW_TEMPLATE) self.assertCountEqual(res.context['table'].data, [domain, ]) self.assertContains(res, "another_test_domain:") formData = {'action': 'domains__clear_domain_context__%s' % domain.id} - res = self.client.post(DOMAINS_INDEX_URL, formData) + res = self.client.post(DOMAINS_INDEX_URL, formData, follow=True) + self.assertRedirects(res, DOMAINS_INDEX_URL, status_code=302, + target_status_code=200, + fetch_redirect_response=True) self.assertTemplateUsed(res, constants.DOMAINS_INDEX_VIEW_TEMPLATE) self.assertCountEqual(res.context['table'].data, self.domains.list()) self.assertNotContains(res, "test_domain:") diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/identity/users/forms.py horizon-20.1.0/openstack_dashboard/dashboards/identity/users/forms.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/identity/users/forms.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/identity/users/forms.py 2021-09-23 09:31:44.000000000 +0000 @@ -296,8 +296,8 @@ strip=False, widget=forms.PasswordInput(render_value=False)) # Reorder form fields from multiple inheritance - self.fields.keyOrder = ["id", "name", "admin_password", - "password", "confirm_password"] + self.field_order = ["id", "name", "admin_password", + "password", "confirm_password"] @sensitive_variables('data', 'password', 'admin_password') def handle(self, request, data): diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/project/instances/tests.py horizon-20.1.0/openstack_dashboard/dashboards/project/instances/tests.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/project/instances/tests.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/project/instances/tests.py 2021-09-23 09:31:44.000000000 +0000 @@ -3454,8 +3454,10 @@ image.min_disk = flavor.disk + 1 keypair = self.keypairs.first() res = self._launch_form_instance(image, flavor, keypair) - msg = "The flavor '%s' is too small" % flavor.name - self.assertContains(res, msg) + msg = (f"The flavor '{flavor.name}' is too small for requested " + f"image. Minimum requirements: {image.min_ram} MB of RAM and " + f"{image.min_disk} GB of Root Disk.") + self.assertContains(res, msg, html=True) def test_launch_form_instance_requirement_error_ram(self): flavor = self.flavors.first() @@ -3464,8 +3466,10 @@ image.min_disk = flavor.disk keypair = self.keypairs.first() res = self._launch_form_instance(image, flavor, keypair) - msg = "The flavor '%s' is too small" % flavor.name - self.assertContains(res, msg) + msg = (f"The flavor '{flavor.name}' is too small for requested " + f"image. Minimum requirements: {image.min_ram} MB of RAM and " + f"{image.min_disk} GB of Root Disk.") + self.assertContains(res, msg, html=True) def test_launch_form_instance_zero_value_flavor_with_min_req(self): flavor = self.flavors.first() @@ -3474,8 +3478,10 @@ image.min_disk = flavor.disk + 1 keypair = self.keypairs.first() res = self._launch_form_instance(image, flavor, keypair) - msg = "The flavor '%s' is too small" % flavor.name - self.assertNotContains(res, msg) + msg = (f"The flavor &39;{flavor.name}&39; is too small for requested " + f"image. Minimum requirements: {image.min_ram} MB of RAM and " + f"{image.min_disk} GB of Root Disk.") + self.assertNotContains(res, msg, html=True) @helpers.create_mocks({api.glance: ('image_list_detailed',), api.neutron: ('network_list', @@ -3707,7 +3713,7 @@ url = reverse('horizon:project:instances:launch') res = self.client.post(url, form_data) - self.assertContains(res, msg) + self.assertContains(res, msg, html=True) self.mock_keypair_list.assert_called_once_with(helpers.IsHttpRequest()) self.mock_security_group_list.assert_called_once_with( @@ -3751,8 +3757,9 @@ def test_launch_form_instance_volume_size_error(self): image = self.versioned_images.get(name='protected_images') volume_size = image.min_disk // 2 - msg = ("The Volume size is too small for the '%s' image" % - image.name) + msg = ("The Volume size is too small for the '%s' image " + "and has to be greater than or equal to '%s' GB." % + (image.name, image.min_disk)) self._test_launch_form_instance_volume_size(image, volume_size, msg) def test_launch_form_instance_non_int_volume_size(self): @@ -3762,7 +3769,8 @@ def test_launch_form_instance_volume_exceed_quota(self): image = self.versioned_images.get(name='protected_images') - msg = "Requested volume exceeds quota: Available: 0, Requested: 1" + msg = ("The requested instance cannot be launched. " + "Requested volume exceeds quota: Available: 0, Requested: 1.") self._test_launch_form_instance_volume_size(image, image.min_disk, msg, 0) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.js horizon-20.1.0/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.js 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/project/static/dashboard/project/workflow/launch-instance/source/source.controller.js 2021-09-23 09:31:44.000000000 +0000 @@ -168,14 +168,16 @@ { id: 'name_or_id', title: gettext('Name'), priority: 1 }, { id: 'updated_at', title: gettext('Updated'), filters: ['simpleDate'], priority: 2 }, { id: 'size', title: gettext('Size'), filters: ['bytes'], priority: 2 }, - { id: 'disk_format', title: gettext('Type'), filters: [getImageDiskFormat], priority: 2 }, + { id: 'disk_format', title: gettext('Format'), + filters: [getImageDiskFormat], priority: 2 }, { id: 'visibility', title: gettext('Visibility'), filters: [getVisibility], priority: 2 } ], snapshot: [ { id: 'name', title: gettext('Name'), priority: 1 }, { id: 'updated_at', title: gettext('Updated'), filters: ['simpleDate'], priority: 2 }, { id: 'size', title: gettext('Size'), filters: ['bytes'], priority: 2 }, - { id: 'disk_format', title: gettext('Type'), filters: [getImageDiskFormat], priority: 2 }, + { id: 'disk_format', title: gettext('Format'), + filters: [getImageDiskFormat], priority: 2 }, { id: 'visibility', title: gettext('Visibility'), filters: [getVisibility], priority: 2 } ], volume: [ @@ -265,7 +267,7 @@ options: statuses }, type: { - label: gettext('Type'), + label: gettext('Format'), name: 'disk_format', singleton: true, options: diskFormats diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/project/vg_snapshots/tests.py horizon-20.1.0/openstack_dashboard/dashboards/project/vg_snapshots/tests.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/project/vg_snapshots/tests.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/project/vg_snapshots/tests.py 2021-09-23 09:31:44.000000000 +0000 @@ -71,11 +71,10 @@ args=[vg_snapshot.id]) res = self.client.post(url, formData) self.assertNoFormErrors(res) - # There are a bunch of backslashes for formatting in the message from - # the response, so remove them when validating the error message. - self.assertIn('Unable to create group "%s" from snapshot.' - % new_cg_name, - res.cookies.output().replace('\\', '')) + self.assertCookieMessage( + res, + 'Unable to create group "%s" from snapshot.' % new_cg_name, + 'Expected failure.') self.assertRedirectsNoFollow(res, INDEX_URL) mock_group_snapshot_get.assert_called_once_with( diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/project/volume_groups/tests.py horizon-20.1.0/openstack_dashboard/dashboards/project/volume_groups/tests.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/dashboards/project/volume_groups/tests.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/dashboards/project/volume_groups/tests.py 2021-09-23 09:31:44.000000000 +0000 @@ -108,8 +108,7 @@ res = self.client.post(url, formData) self.assertNoFormErrors(res) self.assertRedirectsNoFollow(res, INDEX_URL) - self.assertIn("Unable to create group.", - res.cookies.output()) + self.assertCookieMessage(res, "Unable to create group.") self.mock_extension_supported.assert_called_once_with( test.IsHttpRequest(), 'AvailabilityZones') diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/django_pyscss_fix/__init__.py horizon-20.1.0/openstack_dashboard/django_pyscss_fix/__init__.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/django_pyscss_fix/__init__.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/django_pyscss_fix/__init__.py 2021-09-23 09:31:44.000000000 +0000 @@ -12,9 +12,21 @@ import logging import os +import sys + +import django +import six +import six.moves from django.conf import settings +# Temporary workaround for a situation that django-pyscss depends on +# a vendored version of six, django.utils.six which was dropped in Django 3.0. +# TODO(amotoki): Drop the workaround once django-pyscss supports Django 3.0+. +if django.VERSION[0] >= 3: + sys.modules['django.utils.six'] = six + sys.modules['django.utils.six.moves'] = six.moves + scss_asset_root = os.path.join(settings.STATIC_ROOT, 'scss', 'assets') LOG = logging.getLogger(__name__) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/enabled/_1510_project_network_qos_panel.py horizon-20.1.0/openstack_dashboard/enabled/_1510_project_network_qos_panel.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/enabled/_1510_project_network_qos_panel.py 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/enabled/_1510_project_network_qos_panel.py 2021-09-23 09:31:44.000000000 +0000 @@ -8,6 +8,3 @@ # Python panel class of the PANEL to be added. ADD_PANEL = ('openstack_dashboard.dashboards.project.network_qos' '.panel.NetworkQoS') - -# Will default to disabled until the feature is completed. -DISABLED = True diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/locale/ru/LC_MESSAGES/djangojs.po horizon-20.1.0/openstack_dashboard/locale/ru/LC_MESSAGES/djangojs.po --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/locale/ru/LC_MESSAGES/djangojs.po 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/locale/ru/LC_MESSAGES/djangojs.po 2021-09-23 09:31:44.000000000 +0000 @@ -20,16 +20,17 @@ # Ilya Alekseyev , 2019. #zanata # Roman Gorshunov , 2019. #zanata # Dmitriy Rabotyagov , 2020. #zanata +# Roman Gorshunov , 2021. #zanata msgid "" msgstr "" "Project-Id-Version: horizon VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2020-09-16 17:23+0000\n" +"POT-Creation-Date: 2021-09-09 10:31+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2020-09-16 04:40+0000\n" -"Last-Translator: Dmitriy Rabotyagov \n" +"PO-Revision-Date: 2021-09-06 04:35+0000\n" +"Last-Translator: Roman Gorshunov \n" "Language-Team: Russian\n" "Language: ru\n" "X-Generator: Zanata 4.3.3\n" @@ -39,6 +40,10 @@ msgid "#" msgstr "#" +#, python-format +msgid "%s (default)" +msgstr "%s (по умолчанию)" + msgid "1" msgstr "1" @@ -633,6 +638,9 @@ msgid "Confirm Password" msgstr "Подтвердите пароль" +msgid "Confirm password" +msgstr "Подтвердите пароль" + msgid "Connecting" msgstr "Подключение" @@ -726,6 +734,12 @@ msgid "Create New Volume" msgstr "Создать новый диск" +msgid "Create Policy" +msgstr "Создать политику" + +msgid "Create QoS Policy" +msgstr "Создать политику QoS" + msgid "Create Role" msgstr "Создать роль" @@ -2172,6 +2186,9 @@ msgid "Password" msgstr "Пароль" +msgid "Passwords do not match." +msgstr "Пароли не совпадают." + msgid "Pending Delete" msgstr "Ожидает удаления" @@ -2269,6 +2286,9 @@ msgid "Project ID" msgstr " ID проекта" +msgid "Project Name" +msgstr "Имя проекта" + msgid "" "Project networks are created by users.\n" " These networks are fully isolated and are project-specific." @@ -2320,6 +2340,10 @@ msgstr[1] "Политики QoS" msgstr[2] "Политики QoS" +#, python-format +msgid "QoS Policy %s was successfully created." +msgstr "QoS политика %s успешно создана." + msgid "Queued" msgstr "Запланировано" @@ -2533,6 +2557,9 @@ msgid "Select one or more" msgstr "Выберите одну или несколько" +msgid "Select one or more ports" +msgstr "Выберите один или несколько портов" + msgid "Select one or more security groups from the available groups below." msgstr "Выберите одну или более групп безопасности из доступных." @@ -2615,6 +2642,9 @@ msgid "Service type is not enabled: %(desiredType)s" msgstr "Тип службы не включен: %(desiredType)s" +msgid "Set admin password" +msgstr "Пароль администратора" + #, python-format msgid "Setting is not enabled: %(setting)s" msgstr "Настройка не включена: %(setting)s" @@ -3132,6 +3162,9 @@ msgid "Unable to copy the object." msgstr "Не удается скопировать объект." +msgid "Unable to create the QoS Policy." +msgstr "Не удалось создать политику QoS." + msgid "Unable to create the container." msgstr "Не удаётся создать контейнер." diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/locale/ru/LC_MESSAGES/django.po horizon-20.1.0/openstack_dashboard/locale/ru/LC_MESSAGES/django.po --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/locale/ru/LC_MESSAGES/django.po 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/locale/ru/LC_MESSAGES/django.po 2021-09-23 09:31:44.000000000 +0000 @@ -34,16 +34,17 @@ # Ilya Alekseyev , 2019. #zanata # Roman Gorshunov , 2019. #zanata # Dmitriy Rabotyagov , 2020. #zanata +# Roman Gorshunov , 2021. #zanata msgid "" msgstr "" "Project-Id-Version: horizon VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2021-05-11 12:54+0000\n" +"POT-Creation-Date: 2021-09-09 10:31+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2020-09-16 04:39+0000\n" -"Last-Translator: Dmitriy Rabotyagov \n" +"PO-Revision-Date: 2021-09-06 04:32+0000\n" +"Last-Translator: Roman Gorshunov \n" "Language: ru\n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" @@ -1134,6 +1135,9 @@ msgid "Checksum" msgstr "Контрольная сумма" +msgid "Chinese locale rename" +msgstr "Переименование китайской локализации" + msgid "" "Choose \"Disk Format\" for the image. The volume images are created with the " "QEMU disk image utility." @@ -2427,6 +2431,9 @@ msgid "Distributed" msgstr "Распространенный" +msgid "Django launch instance form" +msgstr "Форма запуска инстанса Django" + msgid "" "Do not use a colon ':' with OS::Glance::Images. This resource type does not " "support the use of colons." @@ -3311,6 +3318,12 @@ msgid "Force" msgstr "Принудительная загрузка" +msgid "Force Delete Volume Backup" +msgid_plural "Force Delete Volume Backups" +msgstr[0] "Приундительно удалить снимок диска" +msgstr[1] "Приундительно удалить снимки дисков" +msgstr[2] "Приундительно удалить снимки дисков" + msgid "Force Host Copy" msgstr "Принудительное копирование на узел" @@ -3870,6 +3883,12 @@ msgid "Ingress" msgstr "Входящий трафик" +msgid "Injected File Content (B)" +msgstr "Объём загруженного файла в байтах" + +msgid "Injected File Path (B)" +msgstr "Путь загруженного файла (B)" + msgid "Injected Files" msgstr "Загруженные файлы" @@ -4092,6 +4111,9 @@ msgid "Launch" msgstr "Запустить" +msgid "Launch Index" +msgstr "Индекс запуска" + msgid "Launch Instance" msgstr "Запустить инстанс" @@ -4292,6 +4314,13 @@ msgid "Max. Size (MB) =" msgstr "Макс. размер (МБ) =" +msgid "" +"Maximum Transmission Unit. Minimum is 68 bytes for the IPv4 subnet and 1280 " +"bytes for the IPv6 subnet." +msgstr "" +"Максимальный размер блока передачи (MTU). Минимум - 68 байт для IPv4 " +"подсети, и 1280 байт для IPv6 подсети." + msgid "Memory MB Hours" msgstr "Память МБ-часов" @@ -5543,6 +5572,9 @@ msgid "Rescuing" msgstr "Восстановление" +msgid "Reservation ID" +msgstr "ID резервации" + msgctxt "Current status of a Volume" msgid "Reserved" msgstr "Зарезервировано" @@ -5821,6 +5853,12 @@ msgstr[1] "Запланировано удаление снимков диска" msgstr[2] "Запланировано удаление снимков диска" +msgid "Scheduled forced deletion of Volume Backup" +msgid_plural "Scheduled forced deletion of Volume Backups" +msgstr[0] "Запланировано принудительное удаление снимка диска" +msgstr[1] "Запланировано принудительное удаление снимков диска" +msgstr[2] "Запланировано принудительное удаление снимков диска" + msgid "Scheduled migration (pending confirmation) of Instance" msgid_plural "Scheduled migration (pending confirmation) of Instances" msgstr[0] "Запланирована миграция инстанса (ожидает подтверждения)" @@ -6566,6 +6604,10 @@ msgstr "Успешно обновлена группа безопасности: %s" #, python-format +msgid "Successfully updated volume backup status to \"%s\"." +msgstr "Успешно обновлен статус снимка диска на \"%s\"." + +#, python-format msgid "Successfully updated volume snapshot status: \"%s\"." msgstr "Успешно обновлен статус снимка диска: \"%s\"" @@ -6960,6 +7002,15 @@ msgstr "Указан неверный порт." msgid "" +"The status of a volume backup is normally managed automatically. In some " +"circumstances an administrator may need to explicitly update the status " +"value. This is equivalent to the cinder backup-reset-state command." +msgstr "" +"Статус снимка диска, как правило, управляется автоматически. В некоторых " +"случаях администратору может понадобиться явно указать значение статуса. Это " +"эквивалентно команде cinder snapshot-reset-state." + +msgid "" "The status of a volume is normally managed automatically. In some " "circumstances an administrator may need to explicitly update the status " "value. This is equivalent to the openstack volume set --state " @@ -7295,6 +7346,10 @@ msgid "Unable to clone group." msgstr "Не удалось клонировать группу." +#, python-format +msgid "Unable to confirm resize instance \"%s\"." +msgstr "Не удалось подтвердить изменение размера инстанса \"%s\"." + msgid "Unable to connect to Neutron." msgstr "Невозможно подключиться к Neutron." @@ -7690,6 +7745,9 @@ msgid "Unable to retrieve Neutron quota information." msgstr "Не удалось получить информацию о квотах Neutron." +msgid "Unable to retrieve Nova quota information." +msgstr "Не удалось получить информацию о квотах Nova." + msgid "Unable to retrieve Nova server groups." msgstr "Не удалось получить группы серверов Nova." @@ -8139,6 +8197,12 @@ msgid "Unable to retrieve version information." msgstr "Не удалось получить сведения о версии" +msgid "Unable to retrieve volume backup details." +msgstr "Не удалось получить подробную информацию о снимке диска." + +msgid "Unable to retrieve volume backup project information." +msgstr "Не удалось получить информацию о снимке диска проекта." + msgid "Unable to retrieve volume backups." msgstr "Не удалось получить резервные копии диска." @@ -8238,6 +8302,10 @@ msgid "Unable to retrieve volumes." msgstr "Не удалось получить диски." +#, python-format +msgid "Unable to revert resize instance \"%s\"." +msgstr "Не удалось отменить изменение размера инстанса \"%s\"." + msgid "Unable to set Domain Context." msgstr "Не удалось установить контекст домена." @@ -8325,6 +8393,10 @@ msgid "Unable to update the user." msgstr "Не удалось обновить пользователя." +#, python-format +msgid "Unable to update volume backup status to \"%s\"." +msgstr "Не удалось обновить статус снимка диска на \"%s\"." + msgid "Unable to update volume group." msgstr "Не удалось обновить группу диска." @@ -8529,6 +8601,9 @@ msgid "Update Volume Type Encryption" msgstr "Изменить тип шифрование для Типа Дисков" +msgid "Update Volume backup Status" +msgstr "Обновить статус снимка диска" + msgid "Update a Metadata Namespace" msgstr "Обновить метаданные пространства имён" @@ -8662,6 +8737,9 @@ msgid "User Credentials Details" msgstr "Детали учетных данных пользователя" +msgid "User Data" +msgstr "Данные пользователя" + msgid "User Details" msgstr "Детали пользователя" diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/settings.py horizon-20.1.0/openstack_dashboard/settings.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/settings.py 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/settings.py 2021-09-23 09:31:44.000000000 +0000 @@ -151,9 +151,9 @@ 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', + 'openstack_dashboard.django_pyscss_fix', 'django_pyscss', 'debreach', - 'openstack_dashboard.django_pyscss_fix', 'compressor', 'horizon', 'openstack_auth', diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/app.module.js horizon-20.1.0/openstack_dashboard/static/app/app.module.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/app.module.js 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/app.module.js 2021-09-23 09:31:44.000000000 +0000 @@ -112,8 +112,11 @@ $route ) { - $http.defaults.headers.post['X-CSRFToken'] = $cookies.csrftoken || - $('input[name="csrfmiddlewaretoken"]').val(); + var csrftoken = $cookies.csrftoken || + $('input[name="csrfmiddlewaretoken"]').val(); + $http.defaults.headers.post['X-CSRFToken'] = csrftoken; + $http.defaults.headers.put['X-CSRFToken'] = csrftoken; + $http.defaults.headers.patch['X-CSRFToken'] = csrftoken; // expose the legacy utils module horizon.utils = hzUtils; diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/actions.module.js horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/actions.module.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/actions.module.js 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/actions.module.js 2021-09-23 09:31:44.000000000 +0000 @@ -31,17 +31,30 @@ registerQosActions.$inject = [ 'horizon.framework.conf.resource-type-registry.service', + 'horizon.app.core.network_qos.actions.create.service', 'horizon.app.core.network_qos.actions.delete.service', 'horizon.app.core.network_qos.resourceType' ]; function registerQosActions( registry, + createService, deleteService, qosResourceTypeCode ) { var qosResourceType = registry.getResourceType(qosResourceTypeCode); + qosResourceType.globalActions + .append({ + id: 'createPolicyAction', + service: createService, + template: { + text: gettext('Create Policy'), + type: 'create' + } + } + ); + qosResourceType.itemActions .append({ id: 'deletePolicyAction', diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/actions.module.spec.js 2021-09-23 09:31:44.000000000 +0000 @@ -23,6 +23,11 @@ registry = $injector.get('horizon.framework.conf.resource-type-registry.service'); })); + it('registers Create Policy as a global action', function() { + var actions = registry.getResourceType('OS::Neutron::QoSPolicy').globalActions; + expect(actionHasId(actions, 'createPolicyAction')).toBe(true); + }); + it('registers Delete Policy as an item action', function() { var actions = registry.getResourceType('OS::Neutron::QoSPolicy').itemActions; expect(actionHasId(actions, 'deletePolicyAction')).toBe(true); diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/create.action.service.js horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/create.action.service.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/create.action.service.js 1970-01-01 00:00:00.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/create.action.service.js 2021-09-23 09:31:44.000000000 +0000 @@ -0,0 +1,89 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function() { + 'use strict'; + + /** + * @ngdoc overview + * @ngname horizon.app.core.network_qos.actions.create.service + * + * @description + * Provides all of the actions for creating network qos policy. + */ + + angular + .module('horizon.app.core.network_qos') + .factory('horizon.app.core.network_qos.actions.create.service', createService); + + createService.$inject = [ + 'horizon.app.core.openstack-service-api.neutron', + 'horizon.app.core.openstack-service-api.policy', + 'horizon.app.core.network_qos.actions.workflow.service', + 'horizon.app.core.network_qos.resourceType', + 'horizon.framework.widgets.form.ModalFormService', + 'horizon.framework.widgets.toast.service', + 'horizon.framework.util.actions.action-result.service' + ]; + + function createService( + neutronAPI, + policy, + workflow, + resourceType, + modalFormService, + toast, + actionResultService + ) { + + var service = { + allowed: allowed, + perform: perform, + submit: submit + }; + + return service; + + ////// + function allowed() { + return policy.ifAllowed( + {rules: [ + ['network', 'create_qos_policy'] + ]} + ); + } + + function perform() { + var createPolicy = workflow.init(); + createPolicy.title = gettext('Create QoS Policy'); + return modalFormService.open(createPolicy).then(submit); + } + + function submit(context) { + var data = {name: context.model.name, description: context.model.description, + shared: context.model.shared}; + return neutronAPI.createNetworkQoSPolicy(data).then(onCreateNetworkQoSPolicy); + } + + function onCreateNetworkQoSPolicy(response) { + var qospolicy = response.data; + toast.add('success', interpolate( + gettext('QoS Policy %s was successfully created.'), [qospolicy.name])); + + return actionResultService.getActionResult() + .created(resourceType, qospolicy.id) + .result; + } + } +})(); diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/create.action.service.spec.js horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/create.action.service.spec.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/create.action.service.spec.js 1970-01-01 00:00:00.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/create.action.service.spec.js 2021-09-23 09:31:44.000000000 +0000 @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function() { + 'use strict'; + + describe('horizon.app.core.network_qos.actions.create.service', function() { + + var $q, $scope, neutronAPI, service, modalFormService, policyAPI, toast, resType; + + /////////////////////// + + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.app.core.network_qos')); + + beforeEach(inject(function($injector, _$rootScope_, _$q_) { + $scope = _$rootScope_.$new(); + $q = _$q_; + service = $injector.get('horizon.app.core.network_qos.actions.create.service'); + toast = $injector.get('horizon.framework.widgets.toast.service'); + modalFormService = $injector.get('horizon.framework.widgets.form.ModalFormService'); + neutronAPI = $injector.get('horizon.app.core.openstack-service-api.neutron'); + policyAPI = $injector.get('horizon.app.core.openstack-service-api.policy'); + resType = $injector.get('horizon.app.core.network_qos.resourceType'); + })); + + it('should check if the user is allowed to create netwrok qos policy', function() { + spyOn(policyAPI, 'ifAllowed').and.callThrough(); + var allowed = service.allowed(); + expect(allowed).toBeTruthy(); + expect(policyAPI.ifAllowed).toHaveBeenCalledWith( + { rules: [['network', 'create_qos_policy']] }); + }); + + it('should open the modal', function() { + spyOn(modalFormService, 'open').and.returnValue($q.defer().promise); + spyOn(neutronAPI, 'createNetworkQoSPolicy').and.returnValue($q.defer().promise); + + service.perform(); + $scope.$apply(); + + expect(modalFormService.open).toHaveBeenCalled(); + }); + + it('should submit create neutron qos request to neutron', function() { + var deferred = $q.defer(); + spyOn(neutronAPI, 'createNetworkQoSPolicy').and.returnValue(deferred.promise); + spyOn(toast, 'add').and.callFake(angular.noop); + var handler = jasmine.createSpyObj('handler', ['success']); + + deferred.resolve({data: {name: 'qos1', id: '1'}}); + service.submit({model: {name: 'qos', description: undefined, shared: 'yes'}}) + .then(handler.success); + + $scope.$apply(); + + expect(neutronAPI.createNetworkQoSPolicy).toHaveBeenCalledWith( + {name: 'qos', description: undefined, shared: 'yes'}); + expect(toast.add).toHaveBeenCalledWith( + 'success', 'QoS Policy qos1 was successfully created.'); + + expect(handler.success).toHaveBeenCalled(); + var result = handler.success.calls.first().args[0]; + expect(result.created).toEqual([{type: resType, id: '1'}]); + }); + + }); +})(); diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.js horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.js 1970-01-01 00:00:00.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.js 2021-09-23 09:31:44.000000000 +0000 @@ -0,0 +1,84 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function() { + 'use strict'; + + /** + * @ngdoc factory + * @name horizon.app.core.network_qos.actions.workflow.service + * @ngController + * + * @description + * Workflow for creating network qos policy + */ + + angular + .module('horizon.app.core.network_qos.actions') + .factory('horizon.app.core.network_qos.actions.workflow.service', NetworkQosWorkflow); + + function NetworkQosWorkflow() { + + var workflow = { + init: init + }; + + function init() { + var schema = { + type: 'object', + properties: { + name: { + title: gettext('Name'), + type: 'string' + }, + description: { + title: gettext('Description'), + type: 'string', + maxLength: 255 + }, + shared: { + title: gettext('Shared'), + type: 'boolean', + default: false + } + }, + required: ['name'] + }; + + var form = [ + "name", + { + key: "description", + type: "textarea" + }, + { + key: "shared", + type: "checkbox" + } + ]; + + var model = {}; + + var config = { + schema: schema, + form: form, + model: model + }; + + return config; + } + + return workflow; + } +})(); diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.spec.js horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.spec.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.spec.js 1970-01-01 00:00:00.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/core/network_qos/actions/workflow/workflow.service.spec.js 2021-09-23 09:31:44.000000000 +0000 @@ -0,0 +1,51 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +(function() { + 'use strict'; + + describe('horizon.app.core.network_qos.actions.workflow.service', function() { + + var $q, $scope, workflow, service; + + beforeEach(module('horizon.framework')); + beforeEach(module('horizon.app.core')); + beforeEach(module('horizon.app.core.network_qos')); + + beforeEach(inject(function($injector, _$rootScope_, _$q_) { + $scope = _$rootScope_.$new(); + $q = _$q_; + workflow = $injector.get('horizon.app.core.network_qos.actions.workflow.service'); + service = $injector.get('horizon.app.core.network_qos.actions.create.service'); + })); + + function testInitWorkflow() { + var deferred = $q.defer(); + spyOn(service, 'perform').and.returnValue(deferred.promise); + deferred.resolve({'a1': 'n1'}); + + var config = workflow.init(); + $scope.$apply(); + + expect(config.schema).toBeDefined(); + expect(config.form).toBeDefined(); + expect(config.model).toBeDefined(); + return config; + } + + it('should create workflow config for creation', function() { + testInitWorkflow(); + }); + }); +})(); diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js horizon-20.1.0/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.js 2021-09-23 09:31:44.000000000 +0000 @@ -38,6 +38,7 @@ createNetwork: createNetwork, createSubnet: createSubnet, createTrunk: createTrunk, + createNetworkQoSPolicy: createNetworkQoSPolicy, deletePolicy: deletePolicy, deleteTrunk: deleteTrunk, getAgents: getAgents, @@ -391,6 +392,42 @@ }); } + /** + * @name createNetworkQoSPolicy + * @description + * Create a new network qos policy. + * @returns {Object} The new network qos policy object on success. + * + * @param {Object} newQosPolicy + * The network qos policy to create. Required. + * + * Example new qos policy object + * { + * "name": "myNewNetworkQoSPolicy", + * "description": "new network qos policy", + * "shared": true, + * } + * + * Description of properties on the qos policy object + * + * @property {string} newQosPolicy.name + * The name of the new network qos policy. Required. + * + * @property {string} newQosPolicy.description + * The description of the qos policy. Optional. + * + * @property {boolean} newQosPolicy.shared + * Indicates whether this network qos policy is shared across all other projects. + * By default, it is unchecked (false). Optional. + * + */ + function createNetworkQoSPolicy(newQosPolicy) { + return apiService.post('/api/neutron/qos_policies/', newQosPolicy) + .error(function () { + toastService.add('error', gettext('Unable to create the QoS Policy.')); + }); + } + /** * @name deletePolicy * @description diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js horizon-20.1.0/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/app/core/openstack-service-api/neutron.service.spec.js 2021-09-23 09:31:44.000000000 +0000 @@ -312,6 +312,16 @@ "error": "Unable to retrieve the qos policies." }, { + "func": "createNetworkQoSPolicy", + "method": "post", + "path": "/api/neutron/qos_policies/", + "data": "new network qos policy", + "error": "Unable to create the QoS Policy.", + "testInput": [ + "new network qos policy" + ] + }, + { "func": "deletePolicy", "method": "delete", "path": "/api/neutron/qos_policies/63/", diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/dashboard/scss/components/_membership.scss horizon-20.1.0/openstack_dashboard/static/dashboard/scss/components/_membership.scss --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/static/dashboard/scss/components/_membership.scss 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/static/dashboard/scss/components/_membership.scss 2021-09-23 09:31:44.000000000 +0000 @@ -137,6 +137,8 @@ .role_dropdown { right: 0; left: auto; + max-width: 300px; + overflow-x: auto; & > li { .fa-check { diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/helpers.py horizon-20.1.0/openstack_dashboard/test/helpers.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/helpers.py 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/test/helpers.py 2021-09-23 09:31:44.000000000 +0000 @@ -24,10 +24,12 @@ from unittest import mock from django.conf import settings +from django.contrib.messages.storage import cookie as cookie_storage from django.contrib.messages.storage import default_storage from django.core.handlers import wsgi from django.test.client import RequestFactory from django.test import tag +from django.test import testcases from django import urls from django.utils import http @@ -37,6 +39,7 @@ from horizon import base from horizon import conf +from horizon import exceptions from horizon.test import helpers as horizon_helpers from openstack_dashboard import api from openstack_dashboard import context_processors @@ -278,7 +281,10 @@ Asserts that the given response issued a 302 redirect without processing the view which is redirected to. """ - loc = str(response._headers.get('location', None)[1]) + if response.has_header('location'): + loc = response['location'] + else: + loc = '' loc = http.urlunquote(loc) expected_url = http.urlunquote(expected_url) self.assertEqual(loc, expected_url) @@ -313,10 +319,14 @@ assert len(errors) == count, \ "%d errors were found on the form, %d expected" % \ (len(errors), count) - if message and message not in str(errors): - self.fail("Expected message not found, instead found: %s" - % ["%s: %s" % (key, [e for e in field_errors]) for - (key, field_errors) in errors.items()]) + if message: + text = testcases.assert_and_parse_html( + self, message, None, '"message" contains invalid HTML:') + content = testcases.assert_and_parse_html( + self, str(errors), None, + '"_errors" in the response context is not valid HTML:') + match_count = content.count(text) + self.assertGreaterEqual(match_count, 1) else: assert len(errors) > 0, "No errors were found on the form" @@ -430,6 +440,16 @@ len(errors), 0, "No errors were found on the workflow") + def assertCookieMessage(self, response, expected_msg, detail_msg=None): + data = response.cookies["messages"] + storage = cookie_storage.CookieStorage(None) + messages = [m.message for m in storage._decode(data.value)] + if detail_msg is not None: + _expected = exceptions._append_detail(expected_msg, detail_msg) + else: + _expected = expected_msg + self.assertIn(_expected, messages) + class BaseAdminViewTests(TestCase): """Sets an active user with the "admin" role. diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/integration_tests/regions/menus.py horizon-20.1.0/openstack_dashboard/test/integration_tests/regions/menus.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/integration_tests/regions/menus.py 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/test/integration_tests/regions/menus.py 2021-09-23 09:31:44.000000000 +0000 @@ -149,8 +149,20 @@ else: is_already_within_required_item = True + # In case menu item is collapsed, then below code detects the same + # and disable is_already_within_required_item flag. + # For instance, in case of tacker-horizon, selenium report an + # exception ElementNotInteractableException while opening pages + # under 'NFV Orchestration' panel group. This error occurs when + # element is not clickable or it is not visible yet. + # The panel group 'NFV Orchestration' is never clicked/open hence + # requested pages are not visible/clickable. + item = self._get_item(text, loc_craft_func, src_elem) + if "collapsed" == item.get_attribute( + 'class') and is_already_within_required_item is True: + is_already_within_required_item = False + if not is_already_within_required_item: - item = self._get_item(text, loc_craft_func, src_elem) item.click() if get_selected_func is not None: self._wait_until_transition_ends( diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/test_data/cinder_data.py horizon-20.1.0/openstack_dashboard/test/test_data/cinder_data.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/test_data/cinder_data.py 2021-07-21 20:46:25.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/test/test_data/cinder_data.py 2021-09-23 09:31:44.000000000 +0000 @@ -20,10 +20,12 @@ from cinderclient.v3 import messages from cinderclient.v3 import pools from cinderclient.v3 import qos_specs +from cinderclient.v3 import quotas from cinderclient.v3 import services from cinderclient.v3 import volume_backups as vol_backups from cinderclient.v3 import volume_encryption_types as vol_enc_types from cinderclient.v3 import volume_snapshots as vol_snaps +from cinderclient.v3 import volume_transfers from cinderclient.v3 import volume_type_access from cinderclient.v3 import volume_types from cinderclient.v3 import volumes @@ -33,24 +35,6 @@ from openstack_dashboard.test.test_data import utils from openstack_dashboard.usage import quotas as usage_quotas -# FIXME: workaround for some classes being missing from cinderclient.v3 -# in python-cinderclient versions < 8.0.0. These can become simple -# 'from cinderclient.v3 import xxx' above after we have -# python-cinderclient>=8.0.0 in requirements.txt -try: - # pylint: disable=ungrouped-imports - from cinderclient.v3.quotas import QuotaSet as _qs # noqa - from cinderclient.v3 import quotas # noqa -except ImportError: - from cinderclient.v2 import quotas - -try: - # pylint: disable=ungrouped-imports - from cinderclient.v3.volume_transfers import VolumeTransfer as _vt # noqa - from cinderclient.v3 import volume_transfers # noqa -except ImportError: - from cinderclient.v2 import volume_transfers - def data(TEST): TEST.cinder_messages = utils.TestDataContainer() diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/unit/api/test_glance.py horizon-20.1.0/openstack_dashboard/test/unit/api/test_glance.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/unit/api/test_glance.py 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/test/unit/api/test_glance.py 2021-09-23 09:31:44.000000000 +0000 @@ -33,39 +33,6 @@ @override_settings(API_RESULT_PAGE_SIZE=2) @mock.patch.object(api.glance, 'glanceclient') - def test_long_url(self, mock_glanceclient): - servers = self.servers.list() * 100 - api_images = self.images_api.list() * 100 - instances_img_ids = [instance.image.get('id') for instance in - servers if hasattr(instance, 'image')] - expected_images = self.images.list() * 100 - glanceclient = mock_glanceclient.return_value - mock_images_list = glanceclient.images.list - mock_images_list.return_value = iter(api_images) - - images = api.glance.image_list_detailed_by_ids(self.request, - instances_img_ids) - self.assertEqual(images, expected_images) - - @override_settings(API_RESULT_PAGE_SIZE=2) - @mock.patch.object(api.glance, 'glanceclient') - def test_image_list_detailed_by_ids(self, mock_glanceclient): - servers = self.servers.list() - api_images = self.images_api.list() - instances_img_ids = [instance.image.get('id') for instance in - servers if hasattr(instance, 'image')] - expected_images = self.images.list() - - glanceclient = mock_glanceclient.return_value - mock_images_list = glanceclient.images.list - mock_images_list.return_value = iter(api_images) - - images = api.glance.image_list_detailed_by_ids(self.request, - instances_img_ids) - self.assertEqual(images, expected_images) - - @override_settings(API_RESULT_PAGE_SIZE=2) - @mock.patch.object(api.glance, 'glanceclient') def test_image_list_detailed_no_pagination(self, mock_glanceclient): # Verify that all images are returned even with a small page size api_images = self.images_api.list() diff -Nru horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/unit/api/test_nova.py horizon-20.1.0/openstack_dashboard/test/unit/api/test_nova.py --- horizon-19.2.0+git2021072116.b58ac2894/openstack_dashboard/test/unit/api/test_nova.py 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/openstack_dashboard/test/unit/api/test_nova.py 2021-09-23 09:31:44.000000000 +0000 @@ -784,7 +784,7 @@ self.assertIsInstance(ret, api.nova.Server) self.mock_get_microversion.assert_called_once_with( - mock.sentinel.request, ('instance_description', + mock.sentinel.request, ('multiattach', 'instance_description', 'auto_allocated_network')) self.mock_novaclient.assert_called_once_with( mock.sentinel.request, version=mock.sentinel.microversion) diff -Nru horizon-19.2.0+git2021072116.b58ac2894/PKG-INFO horizon-20.1.0/PKG-INFO --- horizon-19.2.0+git2021072116.b58ac2894/PKG-INFO 2021-07-21 20:46:29.988491500 +0000 +++ horizon-20.1.0/PKG-INFO 2021-09-23 09:32:36.086892100 +0000 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: horizon -Version: 19.2.0.dev63 +Version: 20.1.0 Summary: OpenStack Dashboard Home-page: https://docs.openstack.org/horizon/latest/ Author: OpenStack diff -Nru horizon-19.2.0+git2021072116.b58ac2894/releasenotes/notes/django32-support-78232a850d04842c.yaml horizon-20.1.0/releasenotes/notes/django32-support-78232a850d04842c.yaml --- horizon-19.2.0+git2021072116.b58ac2894/releasenotes/notes/django32-support-78232a850d04842c.yaml 1970-01-01 00:00:00.000000000 +0000 +++ horizon-20.1.0/releasenotes/notes/django32-support-78232a850d04842c.yaml 2021-09-23 09:31:44.000000000 +0000 @@ -0,0 +1,7 @@ +--- +features: + - | + Django 3.2 support is added. + As of Xena release, it is considered as experimental. + Considering Django 2.2 EOL, Django 3.2 will be the default Django + version in Yoga release. diff -Nru horizon-19.2.0+git2021072116.b58ac2894/releasenotes/notes/network_qos-ee068d073e86de76.yaml horizon-20.1.0/releasenotes/notes/network_qos-ee068d073e86de76.yaml --- horizon-19.2.0+git2021072116.b58ac2894/releasenotes/notes/network_qos-ee068d073e86de76.yaml 1970-01-01 00:00:00.000000000 +0000 +++ horizon-20.1.0/releasenotes/notes/network_qos-ee068d073e86de76.yaml 2021-09-23 09:31:44.000000000 +0000 @@ -0,0 +1,5 @@ +--- +features: + - | + Add "Create Network QoS Policy" button to QoS Policy Panel. + From Horizon users can now create network qos policy. diff -Nru horizon-19.2.0+git2021072116.b58ac2894/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po horizon-20.1.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po --- horizon-19.2.0+git2021072116.b58ac2894/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po 2021-09-23 09:31:44.000000000 +0000 @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: horizon\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2021-06-09 08:30+0000\n" +"POT-Creation-Date: 2021-09-09 10:28+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -179,9 +179,6 @@ msgid "19.1.0" msgstr "19.1.0" -msgid "19.1.0-40" -msgstr "19.1.0-40" - msgid "19.2.0" msgstr "19.2.0" diff -Nru horizon-19.2.0+git2021072116.b58ac2894/requirements.txt horizon-20.1.0/requirements.txt --- horizon-19.2.0+git2021072116.b58ac2894/requirements.txt 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/requirements.txt 2021-09-23 09:31:44.000000000 +0000 @@ -7,7 +7,7 @@ # be installed in a specific order. # # PBR should always appear first -pbr!=2.1.0,>=2.0.0 # Apache-2.0 +pbr>=5.5.0 # Apache-2.0 # Horizon Core Requirements Babel>=2.6.0 # BSD @@ -18,19 +18,19 @@ django-pyscss>=2.0.2 # BSD License (2 clause) futurist>=1.2.0 # Apache-2.0 iso8601>=0.1.11 # MIT -keystoneauth1>=3.4.0 # Apache-2.0 +keystoneauth1>=4.3.1 # Apache-2.0 netaddr>=0.7.18 # BSD oslo.concurrency>=3.26.0 # Apache-2.0 oslo.config>=5.2.0 # Apache-2.0 -oslo.i18n>=3.15.3 # Apache-2.0 +oslo.i18n>=5.0.1 # Apache-2.0 oslo.policy>=3.2.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.upgradecheck>=0.1.1 # Apache-2.0 -oslo.utils>=3.40.0 # Apache-2.0 +oslo.utils>=4.8.0 # Apache-2.0 osprofiler>=2.3.0 # Apache-2.0 pymongo!=3.1,>=3.0.2 # Apache-2.0 pyScss>=1.3.7 # MIT License -python-cinderclient>=5.0.0 # Apache-2.0 +python-cinderclient>=8.0.0 # Apache-2.0 python-glanceclient>=2.8.0 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 python-neutronclient>=6.7.0 # Apache-2.0 @@ -38,7 +38,8 @@ python-swiftclient>=3.2.0 # Apache-2.0 pytz>=2013.6 # MIT PyYAML>=3.12 # MIT -requests>=2.14.2 # Apache-2.0 +requests>=2.25.1 # Apache-2.0 +six>=1.16.0 # MIT semantic-version>=2.3.1 # BSD XStatic>=1.0.0 # MIT License XStatic-Angular>=1.5.8.0 # MIT License diff -Nru horizon-19.2.0+git2021072116.b58ac2894/tox.ini horizon-20.1.0/tox.ini --- horizon-19.2.0+git2021072116.b58ac2894/tox.ini 2021-06-28 19:33:51.000000000 +0000 +++ horizon-20.1.0/tox.ini 2021-09-23 09:31:44.000000000 +0000 @@ -1,6 +1,6 @@ [tox] minversion = 3.1 -envlist = pep8,py36,py38,releasenotes,npm +envlist = pep8,py36,py38,releasenotes,npm,py3-dj32 skipsdist = True # Automatic envs (pyXX) will only use the python version appropriate to that # env and ignore basepython inherited from [testenv] if we set @@ -13,8 +13,13 @@ setenv = VIRTUAL_ENV = {envdir} PYTHONDONTWRITEBYTECODE = 1 - PYTHONWARNINGS = once - py{3,36,37}:PYTHONWARNINGS = once,ignore::ImportWarning:backports + PYTHONWARNINGS = once,ignore::PendingDeprecationWarning + py{3,36,37}:PYTHONWARNINGS = once,ignore::PendingDeprecationWarning,ignore::ImportWarning:backports + # DeprecationWarning is disabled in dj32 tox env as Django 3.2 + # deprecated Django 2.2 features. This workaround is just to reduce + # the number of warnings due to this change. + # It should be dropped when we drop Django 2.2 support. + dj32: PYTHONWARNINGS = once,ignore::PendingDeprecationWarning,ignore::DeprecationWarning whitelist_externals = bash @@ -26,6 +31,7 @@ # Unit test for a local hacking rule requires hacking. hacking>=3.0.1,<3.1.0 # Apache-2.0 commands = + dj32: pip install django>=3.2,<3.3 find . -type f -name "*.pyc" -delete bash {toxinidir}/tools/unit_tests.sh {toxinidir} {posargs} diff -Nru horizon-19.2.0+git2021072116.b58ac2894/.zuul.d/django-jobs.yaml horizon-20.1.0/.zuul.d/django-jobs.yaml --- horizon-19.2.0+git2021072116.b58ac2894/.zuul.d/django-jobs.yaml 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/.zuul.d/django-jobs.yaml 2021-09-23 09:31:43.000000000 +0000 @@ -17,15 +17,15 @@ pre-run: playbooks/horizon-tox-django/pre.yaml run: playbooks/horizon-tox-django/run.yaml vars: - tox_envlist: py36 + tox_envlist: py38 required-projects: - name: openstack/horizon - job: - name: horizon-tox-python3-django22 + name: horizon-tox-python3-django32 parent: horizon-tox-python3-django vars: - django_version: '>=2.2,<3.0' + django_version: '>=3.2,<3.3' - project-template: name: horizon-non-primary-django-jobs @@ -37,10 +37,12 @@ # to run tests with different versions of Django. # We specify a job in openstack-python3-xena-jobs(-horizon) # to keep this project template as it is used in horizon plugins. - - openstack-tox-py38 # NOTE: We keep it as a template even though it is not used now. - # - horizon-tox-python3-django22 + - openstack-tox-py38 + - horizon-tox-python3-django32: + voting: false gate: jobs: - openstack-tox-py38 - # - horizon-tox-python3-django22 + # Skip these until the django3.x jobs become voting. + # - horizon-tox-python3-django32 diff -Nru horizon-19.2.0+git2021072116.b58ac2894/.zuul.d/nodejs-jobs.yaml horizon-20.1.0/.zuul.d/nodejs-jobs.yaml --- horizon-19.2.0+git2021072116.b58ac2894/.zuul.d/nodejs-jobs.yaml 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/.zuul.d/nodejs-jobs.yaml 2021-09-23 09:31:43.000000000 +0000 @@ -1,31 +1,4 @@ - job: - name: horizon-nodejs10-run-lint - parent: nodejs-run-lint - description: | - Run lint using Node 10 for horizon plugins. - vars: - node_version: 10 - tox_constraints_file: "{{ ansible_user_dir }}/{{ zuul.projects['opendev.org/openstack/requirements'].src_dir }}/upper-constraints.txt" - nodeset: ubuntu-focal - pre-run: playbooks/horizon-nodejs/pre.yaml - required-projects: - - openstack/horizon - - openstack/requirements - -- job: - name: horizon-nodejs10-run-test - parent: nodejs-run-test-browser - description: | - Run test using Node 10 for horizon plugins. - vars: - node_version: 10 - tox_constraints_file: "{{ ansible_user_dir }}/{{ zuul.projects['opendev.org/openstack/requirements'].src_dir }}/upper-constraints.txt" - pre-run: playbooks/horizon-nodejs/pre.yaml - nodeset: ubuntu-focal - required-projects: - - openstack/horizon - - openstack/requirements -- job: name: horizon-nodejs14-run-lint parent: nodejs-run-lint description: | @@ -54,30 +27,6 @@ - openstack/requirements - project-template: - name: horizon-nodejs10-jobs - description: | - Run lint and test jobs using Node 10 (LTS) for horizon plugins. - check: - jobs: - - horizon-nodejs10-run-lint - - horizon-nodejs10-run-test - gate: - jobs: - - horizon-nodejs10-run-lint - - horizon-nodejs10-run-test - -- project-template: - name: horizon-nodejs10-jobs-nonvoting - description: | - Run lint and test jobs using Node 10 (LTS) for horizon plugins. - check: - jobs: - - horizon-nodejs10-run-lint: - voting: false - - horizon-nodejs10-run-test: - voting: false - -- project-template: name: horizon-nodejs14-jobs description: | Run lint and test jobs using Node 14 (LTS) for horizon plugins. diff -Nru horizon-19.2.0+git2021072116.b58ac2894/.zuul.d/project.yaml horizon-20.1.0/.zuul.d/project.yaml --- horizon-19.2.0+git2021072116.b58ac2894/.zuul.d/project.yaml 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/.zuul.d/project.yaml 2021-09-23 09:31:43.000000000 +0000 @@ -2,7 +2,6 @@ templates: - check-requirements - horizon-cross-jobs - - horizon-nodejs10-jobs - horizon-nodejs14-jobs - horizon-non-primary-django-jobs - openstack-lower-constraints-jobs @@ -28,10 +27,8 @@ jobs: - horizon-integration-tests-xstatic-master - horizon-tox-py36-xstatic-master - - horizon-nodejs10-run-test-xstatic-master - horizon-nodejs14-run-test-xstatic-master periodic: jobs: - - horizon-nodejs10-run-test - horizon-nodejs14-run-test - horizon-integration-tests diff -Nru horizon-19.2.0+git2021072116.b58ac2894/.zuul.d/xstatic-master.yaml horizon-20.1.0/.zuul.d/xstatic-master.yaml --- horizon-19.2.0+git2021072116.b58ac2894/.zuul.d/xstatic-master.yaml 2021-06-28 19:33:50.000000000 +0000 +++ horizon-20.1.0/.zuul.d/xstatic-master.yaml 2021-09-23 09:31:43.000000000 +0000 @@ -31,12 +31,6 @@ required-projects: *xstatic-projects - job: - name: horizon-nodejs10-run-test-xstatic-master - parent: horizon-nodejs10-run-test - required-projects: *xstatic-projects - pre-run: playbooks/npm-test-xstatic-master/pre.yaml - -- job: name: horizon-nodejs14-run-test-xstatic-master parent: horizon-nodejs14-run-test required-projects: *xstatic-projects