katello 4.12.0.rc3 → 4.13.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of katello might be problematic. Click here for more details.

Files changed (244) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -1
  3. data/app/assets/javascripts/katello/locale/bn/katello.js +3365 -3350
  4. data/app/assets/javascripts/katello/locale/bn_IN/katello.js +3136 -3121
  5. data/app/assets/javascripts/katello/locale/ca/katello.js +3588 -3576
  6. data/app/assets/javascripts/katello/locale/cs/katello.js +3499 -3487
  7. data/app/assets/javascripts/katello/locale/cs_CZ/katello.js +4186 -4186
  8. data/app/assets/javascripts/katello/locale/de/katello.js +5553 -5562
  9. data/app/assets/javascripts/katello/locale/de_AT/katello.js +3008 -2993
  10. data/app/assets/javascripts/katello/locale/de_DE/katello.js +3066 -3051
  11. data/app/assets/javascripts/katello/locale/el/katello.js +3376 -3370
  12. data/app/assets/javascripts/katello/locale/en/katello.js +3008 -2993
  13. data/app/assets/javascripts/katello/locale/en_GB/katello.js +3076 -3073
  14. data/app/assets/javascripts/katello/locale/en_US/katello.js +3008 -2993
  15. data/app/assets/javascripts/katello/locale/es/katello.js +5366 -5372
  16. data/app/assets/javascripts/katello/locale/et_EE/katello.js +3008 -2993
  17. data/app/assets/javascripts/katello/locale/fr/katello.js +5975 -5984
  18. data/app/assets/javascripts/katello/locale/gl/katello.js +3125 -3113
  19. data/app/assets/javascripts/katello/locale/gu/katello.js +3119 -3104
  20. data/app/assets/javascripts/katello/locale/he_IL/katello.js +3020 -3005
  21. data/app/assets/javascripts/katello/locale/hi/katello.js +3137 -3122
  22. data/app/assets/javascripts/katello/locale/id/katello.js +3008 -2993
  23. data/app/assets/javascripts/katello/locale/it/katello.js +4469 -4466
  24. data/app/assets/javascripts/katello/locale/ja/katello.js +5969 -5978
  25. data/app/assets/javascripts/katello/locale/ka/katello.js +5649 -5652
  26. data/app/assets/javascripts/katello/locale/kn/katello.js +3136 -3121
  27. data/app/assets/javascripts/katello/locale/ko/katello.js +4717 -4720
  28. data/app/assets/javascripts/katello/locale/locale/katello.js +1050 -1084
  29. data/app/assets/javascripts/katello/locale/ml_IN/katello.js +3008 -2993
  30. data/app/assets/javascripts/katello/locale/mr/katello.js +3136 -3121
  31. data/app/assets/javascripts/katello/locale/nl_NL/katello.js +3116 -3101
  32. data/app/assets/javascripts/katello/locale/or/katello.js +3137 -3122
  33. data/app/assets/javascripts/katello/locale/pa/katello.js +3136 -3121
  34. data/app/assets/javascripts/katello/locale/pl/katello.js +3210 -3195
  35. data/app/assets/javascripts/katello/locale/pl_PL/katello.js +3008 -2993
  36. data/app/assets/javascripts/katello/locale/pt/katello.js +3009 -2994
  37. data/app/assets/javascripts/katello/locale/pt_BR/katello.js +5362 -5368
  38. data/app/assets/javascripts/katello/locale/ro/katello.js +3008 -2993
  39. data/app/assets/javascripts/katello/locale/ro_RO/katello.js +3008 -2993
  40. data/app/assets/javascripts/katello/locale/ru/katello.js +4638 -4641
  41. data/app/assets/javascripts/katello/locale/sl/katello.js +3051 -3036
  42. data/app/assets/javascripts/katello/locale/sv_SE/katello.js +3156 -3144
  43. data/app/assets/javascripts/katello/locale/ta/katello.js +3365 -3350
  44. data/app/assets/javascripts/katello/locale/ta_IN/katello.js +3121 -3106
  45. data/app/assets/javascripts/katello/locale/te/katello.js +3136 -3121
  46. data/app/assets/javascripts/katello/locale/tr/katello.js +3025 -3010
  47. data/app/assets/javascripts/katello/locale/vi/katello.js +3008 -2993
  48. data/app/assets/javascripts/katello/locale/vi_VN/katello.js +3008 -2993
  49. data/app/assets/javascripts/katello/locale/zh/katello.js +3008 -2993
  50. data/app/assets/javascripts/katello/locale/zh_CN/katello.js +5968 -5977
  51. data/app/assets/javascripts/katello/locale/zh_TW/katello.js +4694 -4697
  52. data/app/assets/javascripts/katello/sync_management/sync_management.js +1 -0
  53. data/app/controllers/katello/api/registry/registry_proxies_controller.rb +51 -124
  54. data/app/controllers/katello/api/rhsm/candlepin_dynflow_proxy_controller.rb +12 -20
  55. data/app/controllers/katello/api/v2/activation_keys_controller.rb +10 -4
  56. data/app/controllers/katello/api/v2/capsule_content_controller.rb +24 -0
  57. data/app/controllers/katello/api/v2/content_view_versions_controller.rb +9 -2
  58. data/app/controllers/katello/api/v2/debs_controller.rb +1 -1
  59. data/app/controllers/katello/api/v2/errata_controller.rb +1 -1
  60. data/app/controllers/katello/api/v2/host_subscriptions_controller.rb +12 -4
  61. data/app/controllers/katello/api/v2/hosts_bulk_actions_controller.rb +3 -3
  62. data/app/controllers/katello/api/v2/organizations_controller.rb +0 -11
  63. data/app/controllers/katello/api/v2/packages_controller.rb +1 -1
  64. data/app/controllers/katello/api/v2/products_bulk_actions_controller.rb +1 -1
  65. data/app/controllers/katello/api/v2/repositories_controller.rb +18 -12
  66. data/app/controllers/katello/api/v2/repository_sets_controller.rb +2 -1
  67. data/app/controllers/katello/api/v2/simple_content_access_controller.rb +9 -22
  68. data/app/controllers/katello/concerns/api/v2/authorization.rb +1 -1
  69. data/app/helpers/katello/concerns/dashboard_helper_extensions.rb +0 -10
  70. data/app/helpers/katello/hosts_and_hostgroups_helper.rb +14 -2
  71. data/app/helpers/katello/katello_urls_helper.rb +26 -1
  72. data/app/helpers/katello/subscription_mailer_helper.rb +1 -1
  73. data/app/jobs/create_manifest_expire_soon_warning_notifications.rb +11 -0
  74. data/app/lib/actions/candlepin/owner/regenerate_upstream_identity_cert.rb +21 -0
  75. data/app/lib/actions/katello/capsule_content/sync.rb +1 -1
  76. data/app/lib/actions/katello/capsule_content/verify_checksum.rb +75 -0
  77. data/app/lib/actions/katello/content_view/promote.rb +1 -1
  78. data/app/lib/actions/katello/content_view/publish.rb +1 -1
  79. data/app/lib/actions/katello/content_view_version/verify_checksum.rb +29 -0
  80. data/app/lib/actions/katello/host/hypervisors_update.rb +1 -0
  81. data/app/lib/actions/katello/host/update_content_view.rb +2 -2
  82. data/app/lib/actions/katello/organization/manifest_import.rb +5 -0
  83. data/app/lib/actions/katello/organization/manifest_refresh.rb +3 -0
  84. data/app/lib/actions/katello/repository/metadata_generate.rb +7 -1
  85. data/app/lib/actions/katello/repository/remove_content.rb +1 -0
  86. data/app/lib/actions/katello/repository/sync.rb +2 -1
  87. data/app/lib/actions/katello/repository/upload_files.rb +1 -0
  88. data/app/lib/actions/pulp3/capsule_content/verify_checksum.rb +27 -0
  89. data/app/lib/actions/pulp3/orchestration/content_view_version/export_repository.rb +7 -9
  90. data/app/lib/actions/pulp3/orchestration/content_view_version/syncable_export.rb +5 -4
  91. data/app/lib/katello/concerns/base_template_scope_extensions.rb +7 -2
  92. data/app/lib/katello/errors.rb +4 -0
  93. data/app/lib/katello/http_resource.rb +6 -1
  94. data/app/lib/katello/resources/candlepin/consumer.rb +1 -1
  95. data/app/lib/katello/resources/candlepin/upstream_consumer.rb +18 -6
  96. data/app/lib/katello/resources/candlepin/upstream_job.rb +1 -1
  97. data/app/lib/katello/resources/cdn.rb +4 -13
  98. data/app/lib/katello/resources/registry.rb +25 -0
  99. data/app/mailers/katello/subscription_mailer.rb +3 -6
  100. data/app/models/katello/candlepin/repository_mapper.rb +1 -1
  101. data/app/models/katello/concerns/organization_extensions.rb +42 -3
  102. data/app/models/katello/content_view.rb +28 -0
  103. data/app/models/katello/content_view_environment_content_facet.rb +4 -2
  104. data/app/models/katello/glue/provider.rb +19 -12
  105. data/app/models/katello/glue/pulp/repos.rb +3 -8
  106. data/app/models/katello/host/content_facet.rb +1 -1
  107. data/app/models/katello/host/subscription_facet.rb +1 -1
  108. data/app/models/katello/host_collection.rb +12 -3
  109. data/app/models/katello/ping.rb +1 -1
  110. data/app/models/katello/repository.rb +33 -0
  111. data/app/models/katello/root_repository.rb +0 -4
  112. data/app/services/katello/content_unit_indexer.rb +9 -0
  113. data/app/services/katello/pulp3/alternate_content_source.rb +6 -8
  114. data/app/services/katello/pulp3/api/core.rb +13 -0
  115. data/app/services/katello/pulp3/api/yum.rb +11 -0
  116. data/app/services/katello/pulp3/docker_manifest.rb +5 -1
  117. data/app/services/katello/pulp3/repository/generic.rb +1 -1
  118. data/app/services/katello/pulp3/repository.rb +26 -6
  119. data/app/services/katello/pulp3/repository_mirror.rb +13 -12
  120. data/app/services/katello/pulp3/service_common.rb +2 -10
  121. data/app/services/katello/pulp3/smart_proxy_repository.rb +0 -2
  122. data/app/services/katello/ui_notifications/subscriptions/manifest_expire_soon_warning.rb +75 -0
  123. data/app/views/foreman/job_templates/update_package_-_katello_ansible_default.erb +5 -1
  124. data/app/views/foreman/job_templates/update_packages_by_search_query_-_katello_ansible_default.erb +2 -2
  125. data/app/views/foreman/job_templates/upload_profile.erb +16 -0
  126. data/app/views/katello/api/v2/content_view_filter_rules/show.json.rabl +9 -0
  127. data/app/views/katello/api/v2/docker_manifests/show.json.rabl +1 -0
  128. data/app/views/katello/api/v2/hosts/host_collections.json.rabl +5 -1
  129. data/app/views/katello/api/v2/organizations/show.json.rabl +9 -1
  130. data/app/views/katello/hosts/_errata_counts.html.erb +1 -1
  131. data/app/views/overrides/activation_keys/_host_environment_select.html.erb +1 -1
  132. data/app/views/overrides/activation_keys/_host_media_type_select.html.erb +15 -5
  133. data/app/views/overrides/activation_keys/_host_tab_pane.html.erb +1 -29
  134. data/config/routes/api/registry.rb +4 -8
  135. data/config/routes/api/v2.rb +2 -0
  136. data/db/migrate/20240423112842_add_fields_to_katello_docker_manifest.rb +8 -0
  137. data/db/migrate/20240502192021_change_katello_repository_rpms_id_seq_to_big_int.rb +9 -0
  138. data/db/seeds.d/109-katello-notification-blueprints.rb +6 -0
  139. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/activation-keys/details/activation-key-repository-sets.controller.js +3 -3
  140. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-credentials/new/views/new-content-credential.html +2 -1
  141. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/content-hosts/details/content-host-repository-sets.controller.js +3 -3
  142. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/i18n/bastion_katello.pot +0 -15
  143. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/details/views/repository-info.html +8 -6
  144. data/engines/bastion_katello/app/assets/javascripts/bastion_katello/products/details/repositories/new/views/new-repository.html +12 -10
  145. data/lib/katello/permission_creator.rb +3 -3
  146. data/lib/katello/permissions/registry_permissions.rb +4 -7
  147. data/lib/katello/plugin.rb +10 -16
  148. data/lib/katello/repository_types/ostree.rb +7 -0
  149. data/lib/katello/scheduled_jobs.rb +7 -1
  150. data/lib/katello/tasks/clean_backend_objects.rake +1 -1
  151. data/lib/katello/tasks/repository.rake +22 -0
  152. data/lib/katello/version.rb +1 -1
  153. data/locale/action_names.rb +4 -3
  154. data/locale/bn/katello.po +166 -151
  155. data/locale/bn_IN/katello.po +166 -151
  156. data/locale/ca/katello.po +166 -151
  157. data/locale/cs/katello.po +166 -151
  158. data/locale/cs_CZ/LC_MESSAGES/katello.mo +0 -0
  159. data/locale/cs_CZ/katello.po +172 -157
  160. data/locale/de/LC_MESSAGES/katello.mo +0 -0
  161. data/locale/de/katello.po +178 -163
  162. data/locale/de_AT/katello.po +166 -151
  163. data/locale/de_DE/katello.po +166 -151
  164. data/locale/el/katello.po +166 -151
  165. data/locale/en/katello.po +166 -151
  166. data/locale/en_GB/katello.po +166 -151
  167. data/locale/en_US/katello.po +166 -151
  168. data/locale/es/LC_MESSAGES/katello.mo +0 -0
  169. data/locale/es/katello.po +178 -163
  170. data/locale/et_EE/katello.po +166 -151
  171. data/locale/fr/LC_MESSAGES/katello.mo +0 -0
  172. data/locale/fr/katello.po +179 -164
  173. data/locale/gl/katello.po +166 -151
  174. data/locale/gu/katello.po +166 -151
  175. data/locale/he_IL/katello.po +166 -151
  176. data/locale/hi/katello.po +166 -151
  177. data/locale/id/katello.po +166 -151
  178. data/locale/it/LC_MESSAGES/katello.mo +0 -0
  179. data/locale/it/katello.po +169 -154
  180. data/locale/ja/LC_MESSAGES/katello.mo +0 -0
  181. data/locale/ja/katello.po +179 -164
  182. data/locale/ka/LC_MESSAGES/katello.mo +0 -0
  183. data/locale/ka/katello.po +177 -162
  184. data/locale/katello.pot +1119 -1062
  185. data/locale/kn/katello.po +166 -151
  186. data/locale/ko/LC_MESSAGES/katello.mo +0 -0
  187. data/locale/ko/katello.po +174 -159
  188. data/locale/ml_IN/katello.po +166 -151
  189. data/locale/mr/katello.po +166 -151
  190. data/locale/nl_NL/katello.po +166 -151
  191. data/locale/or/katello.po +166 -151
  192. data/locale/pa/katello.po +166 -151
  193. data/locale/pl/katello.po +166 -151
  194. data/locale/pl_PL/katello.po +166 -151
  195. data/locale/pt/katello.po +166 -151
  196. data/locale/pt_BR/LC_MESSAGES/katello.mo +0 -0
  197. data/locale/pt_BR/katello.po +178 -163
  198. data/locale/ro/katello.po +166 -151
  199. data/locale/ro_RO/katello.po +166 -151
  200. data/locale/ru/LC_MESSAGES/katello.mo +0 -0
  201. data/locale/ru/katello.po +171 -156
  202. data/locale/sl/katello.po +166 -151
  203. data/locale/sv_SE/katello.po +166 -151
  204. data/locale/ta/katello.po +166 -151
  205. data/locale/ta_IN/katello.po +166 -151
  206. data/locale/te/katello.po +166 -151
  207. data/locale/tr/katello.po +166 -151
  208. data/locale/vi/katello.po +166 -151
  209. data/locale/vi_VN/katello.po +166 -151
  210. data/locale/zh/katello.po +166 -151
  211. data/locale/zh_CN/LC_MESSAGES/katello.mo +0 -0
  212. data/locale/zh_CN/katello.po +179 -164
  213. data/locale/zh_TW/LC_MESSAGES/katello.mo +0 -0
  214. data/locale/zh_TW/katello.po +171 -156
  215. data/webpack/ForemanColumnExtensions/index.js +129 -0
  216. data/webpack/components/ActivationKeysSearch/ActivationKeysSearch.test.js +28 -0
  217. data/webpack/components/ActivationKeysSearch/index.js +222 -0
  218. data/webpack/components/Table/TableWrapper.js +14 -0
  219. data/webpack/components/extensions/HostDetails/ActionsBar/index.js +1 -1
  220. data/webpack/components/extensions/HostDetails/Tabs/PackagesTab/PackagesTab.js +1 -1
  221. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packageInstallModal.test.js +1 -0
  222. data/webpack/components/extensions/HostDetails/Tabs/__tests__/packagesTab.test.js +1 -0
  223. data/webpack/components/extensions/Hosts/ActionsBar/index.js +20 -1
  224. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/BulkChangeHostCVModal.js +220 -0
  225. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/actions.js +23 -0
  226. data/webpack/components/extensions/Hosts/BulkActions/BulkChangeHostCVModal/index.js +25 -0
  227. data/webpack/components/extensions/Hosts/BulkActions/__tests__/bulkChangeHostCVModal.test.js +133 -0
  228. data/webpack/global_index.js +19 -0
  229. data/webpack/scenes/ContentViews/Details/ComponentContentViews/ContentViewComponents.js +6 -3
  230. data/webpack/scenes/ContentViews/Publish/CVPublishForm.js +1 -1
  231. data/webpack/scenes/ContentViews/Publish/__tests__/publishContentView.test.js +30 -0
  232. data/webpack/scenes/Hosts/ChangeContentSource/actions.js +3 -1
  233. data/webpack/scenes/Hosts/ChangeContentSource/components/ContentSourceForm.js +63 -25
  234. data/webpack/scenes/Hosts/ChangeContentSource/index.js +24 -16
  235. data/webpack/scenes/RedHatRepositories/__tests__/__snapshots__/RedHatRepositoriesPage.test.js.snap +1 -0
  236. data/webpack/scenes/Subscriptions/Manifest/ManageManifestModal.js +64 -5
  237. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/UpstreamSubscriptionsPage.js +16 -13
  238. data/webpack/scenes/Subscriptions/UpstreamSubscriptions/__tests__/__snapshots__/UpstreamSubscriptionsPage.test.js.snap +14 -8
  239. data/webpack/scenes/Subscriptions/__tests__/__snapshots__/SubscriptionsPage.test.js.snap +1 -0
  240. data/webpack/scenes/Subscriptions/components/SubscriptionsToolbar/SubscriptionsToolbar.js +1 -1
  241. metadata +59 -40
  242. data/app/assets/javascripts/katello/hosts/activation_key_edit.js +0 -167
  243. data/app/lib/actions/katello/host/upload_package_profile.rb +0 -45
  244. data/app/lib/actions/katello/host/upload_profiles.rb +0 -47
@@ -91,16 +91,21 @@ module Katello
91
91
  end
92
92
 
93
93
  def remote_partial_update
94
- if remote_options[:url]&.start_with?('uln')
95
- api.remotes_uln_api.partial_update(repo.remote_href, remote_options)
96
- else
97
- api.remotes_api.partial_update(repo.remote_href, remote_options)
94
+ url_type = remote_options[:url]&.start_with?('uln') ? 'uln' : 'default'
95
+ remote_type = repo.remote_href.start_with?('/pulp/api/v3/remotes/rpm/uln/') ? 'uln' : 'default'
96
+ href = repo.remote_href
97
+
98
+ if url_type == remote_type
99
+ api.get_remotes_api(href: href).partial_update(href, remote_options)
100
+ else # We need to recreate a remote of the correct type!
101
+ create_remote
102
+ delete_remote(href: href)
98
103
  end
99
104
  end
100
105
 
101
106
  def delete_remote(options = {})
102
107
  options[:href] ||= repo.remote_href
103
- ignore_404_exception { remote_options[:url]&.start_with?('uln') ? api.remotes_uln_api.delete(options[:href]) : api.remotes_api.delete(options[:href]) } if options[:href]
108
+ ignore_404_exception { api.get_remotes_api(href: options[:href]).delete(options[:href]) } if options[:href]
104
109
  end
105
110
 
106
111
  def self.instance_for_type(repo, smart_proxy)
@@ -139,7 +144,7 @@ module Katello
139
144
  end
140
145
 
141
146
  def get_remote(href = repo.remote_href)
142
- repo.url&.start_with?('uln') ? api.remotes_uln_api.read(href) : api.remotes_api.read(href)
147
+ api.get_remotes_api(href: href).read(href)
143
148
  end
144
149
 
145
150
  def get_distribution(href = distribution_reference.href)
@@ -210,6 +215,10 @@ module Katello
210
215
  api.publications_api.create(publication_data)
211
216
  end
212
217
 
218
+ def delete_publication
219
+ ignore_404_exception { api.publications_api.delete(repo.publication_href) } if repo.publication_href
220
+ end
221
+
213
222
  def publication_options(repository_version)
214
223
  {
215
224
  repository_version: repository_version
@@ -329,6 +338,17 @@ module Katello
329
338
 
330
339
  def delete_version
331
340
  ignore_404_exception { api.repository_versions_api.delete(repo.version_href) } unless version_zero?
341
+ rescue api.api_exception_class => e
342
+ if e.message.include?("are currently being used to distribute content")
343
+ Rails.logger.warn "Exception when calling repository_versions_api->delete: #{e}"
344
+ publication_href = repo.publication_href
345
+ Rails.logger.warn "Trying to delete publication #{publication_href} for repository #{repo.id}}"
346
+ Rails.logger.error "Could not delete version: #{repo.version_href} because conflicting publication could not be looked up" unless publication_href
347
+ if publication_href
348
+ ignore_404_exception { api.publications_api.delete(publication_href) }
349
+ ignore_404_exception { api.repository_versions_api.delete(repo.version_href) }
350
+ end
351
+ end
332
352
  end
333
353
 
334
354
  def create_version(options = {})
@@ -24,11 +24,9 @@ module Katello
24
24
  def refresh_entities
25
25
  href = remote_href
26
26
  if href
27
- if remote_options[:url]&.start_with?('uln')
28
- [api.remotes_uln_api.partial_update(href, remote_options)]
29
- else
30
- [api.remotes_api.partial_update(href, remote_options)]
31
- end
27
+ # Do not consider remotes_uln_api, since the Katello server is not a ULN server. Even if the sync
28
+ # to Katello used ULN, the sync from Katello server to smart proxy will use a normal RPM remote!
29
+ [api.remotes_api.partial_update(href, remote_options)]
32
30
  else
33
31
  create_remote
34
32
  []
@@ -111,13 +109,10 @@ module Katello
111
109
  end
112
110
 
113
111
  def create_remote
114
- if remote_options[:url]&.start_with?('uln')
115
- remote_file_data = @repo_service.api.class.remote_uln_class.new(remote_options)
116
- api.remotes_uln_api.create(remote_file_data)
117
- else
118
- remote_file_data = @repo_service.api.remote_class.new(remote_options)
119
- api.remotes_api.create(remote_file_data)
120
- end
112
+ # Do not consider remotes_uln_api, since the Katello server is not a ULN server. Even if the sync
113
+ # to Katello used ULN, the sync from Katello server to smart proxy will use a normal RPM remote!
114
+ remote_file_data = @repo_service.api.remote_class.new(remote_options)
115
+ api.remotes_api.create(remote_file_data)
121
116
  end
122
117
 
123
118
  def compute_remote_options
@@ -240,6 +235,12 @@ module Katello
240
235
  distribution_data = api.distribution_class.new(distribution_options(path))
241
236
  repo_service.distributions_api.create(distribution_data)
242
237
  end
238
+
239
+ def repair
240
+ data = api.repair_class.new
241
+ fail "Could not lookup a version_href for repo #{repo_service.repo.id}" if version_href.nil?
242
+ api.repository_versions_api.repair(version_href, data)
243
+ end
243
244
  end
244
245
  end
245
246
  end
@@ -9,11 +9,7 @@ module Katello
9
9
  remote_file_data = api.remote_class.new(remote_options)
10
10
  end
11
11
  reformat_api_exception do
12
- if remote_options[:url]&.start_with?('uln')
13
- response = api.remotes_uln_api.create(remote_file_data)
14
- else
15
- response = api.remotes_api.create(remote_file_data)
16
- end
12
+ response = api.get_remotes_api(url: remote_options[:url]).create(remote_file_data)
17
13
  end
18
14
  response
19
15
  end
@@ -37,11 +33,7 @@ module Katello
37
33
  end
38
34
 
39
35
  reformat_api_exception do
40
- if remote_options[:url]&.start_with?('uln')
41
- response = api.remotes_uln_api.create(remote_file_data)
42
- else
43
- response = api.remotes_api.create(remote_file_data)
44
- end
36
+ response = api.get_remotes_api(url: remote_options[:url]).create(remote_file_data)
45
37
  #delete is async, but if its not properly deleted, orphan cleanup will take care of it later
46
38
  delete_remote(href: response.pulp_href)
47
39
  end
@@ -35,13 +35,11 @@ module Katello
35
35
 
36
36
  def delete_orphan_repository_versions
37
37
  tasks = []
38
-
39
38
  orphan_repository_versions.each do |api, version_hrefs|
40
39
  tasks << version_hrefs.collect do |href|
41
40
  api.repository_versions_api.delete(href)
42
41
  end
43
42
  end
44
-
45
43
  tasks.flatten
46
44
  end
47
45
 
@@ -0,0 +1,75 @@
1
+ module Katello
2
+ module UINotifications
3
+ module Subscriptions
4
+ class ManifestExpireSoonWarning
5
+ class << self
6
+ def deliver!
7
+ ::Organization.unscoped.all.each do |organization|
8
+ if (notification = existing_notification(organization))
9
+ days_remaining = organization.manifest_expire_days_remaining
10
+ if days_remaining == 0 || days_remaining > Setting[:expire_soon_days].to_i
11
+ # if the manifest has already expired, delete the notification;
12
+ # user will have a ManifestExpiredWarning instead.
13
+ # If user changes the expire_soon_days setting, remove notifications
14
+ # that are no longer relevant.
15
+ Rails.logger.debug("ManifestExpireSoonWarning: deleting notification for #{organization.name}")
16
+ notification.destroy
17
+ next
18
+ end
19
+ # don't update if the message hasn't changed
20
+ next unless message(organization).to_s !=
21
+ notification.message.to_s
22
+ notification.update(
23
+ :message => message(organization),
24
+ :actions => actions
25
+ )
26
+ else
27
+ next unless organization.manifest_expiring_soon?
28
+ ::Notification.create!(
29
+ :subject => organization,
30
+ :initiator => User.anonymous_admin,
31
+ :audience => Notification::AUDIENCE_SUBJECT,
32
+ :message => message(organization),
33
+ :actions => actions,
34
+ :notification_blueprint => blueprint
35
+ )
36
+ end
37
+ end
38
+ end
39
+
40
+ def existing_notification(subject)
41
+ matching_notification = Notification.unscoped.find_by(:subject => subject, :notification_blueprint => blueprint)
42
+ return false if matching_notification.blank?
43
+ matching_notification
44
+ end
45
+
46
+ def message(organization)
47
+ ::UINotifications::StringParser.new(
48
+ blueprint.message,
49
+ :manifest_expire_date => organization.manifest_expiration_date&.to_date,
50
+ :subject => organization,
51
+ :days_remaining => organization.manifest_expire_days_remaining
52
+ )
53
+ end
54
+
55
+ def actions
56
+ {
57
+ :links => [
58
+ {
59
+ :href => "/subscriptions",
60
+ :title => _('Subscriptions'),
61
+ :external => false
62
+ }
63
+ ]
64
+ }
65
+ end
66
+
67
+ def blueprint
68
+ @blueprint ||= NotificationBlueprint.unscoped.find_by(
69
+ :name => 'manifest_expire_soon_warning')
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -13,4 +13,8 @@ provider_type: Ansible
13
13
  kind: job_template
14
14
  %>
15
15
 
16
- <%= render_template('Run Command - Ansible Default', :command => "yum -y update #{input('package')}") %>
16
+ <%
17
+ pkgs = input('package')
18
+ pkgs = '"*"' if pkgs.empty?
19
+ -%>
20
+ <%= render_template('Package Action - Ansible Default', :state => 'latest', :name => pkgs) %>
@@ -3,7 +3,7 @@ kind: job_template
3
3
  name: Update packages by search query - Katello Ansible Default
4
4
  job_category: Katello
5
5
  description_format: 'Update package(s) %{Packages search query}'
6
- feature: katello_package_remove_by_search
6
+ feature: katello_package_update_by_search
7
7
  provider_type: Ansible
8
8
  template_inputs:
9
9
  - name: Packages search query
@@ -22,7 +22,7 @@ template_inputs:
22
22
  versions: input('Selected update versions')
23
23
  ) -%>
24
24
  <% if package_names.empty? -%>
25
- <%= render_template('Run Command - Ansible Default', :command => "yum -y update") %>
25
+ <%= render_template('Package Action - Ansible Default', :state => 'latest', :name => '"*"') %>
26
26
  <% else -%>
27
27
  ---
28
28
  - hosts: all
@@ -0,0 +1,16 @@
1
+ <%#
2
+ kind: job_template
3
+ name: Upload profile - Katello Script Default
4
+ job_category: Katello
5
+ model: JobTemplate
6
+ provider_type: script
7
+ description_format: Upload package profile for a host
8
+ feature: katello_upload_profile
9
+ %>
10
+ #!/bin/sh
11
+ <% if @host.operatingsystem.family == 'Redhat' -%>
12
+ dnf uploadprofile --force-upload
13
+ <% else -%>
14
+ package-profile-upload --force-upload
15
+ <% end -%>
16
+ subscription-manager repos
@@ -15,4 +15,13 @@ attributes :architecture, :if => lambda { |rule| rule.respond_to?(:architecture)
15
15
  attributes :types, :if => lambda { |rule| rule.respond_to?(:types) && !rule.types.blank? }
16
16
  attributes :date_type, :if => lambda { |rule| rule.respond_to?(:date_type) }
17
17
  attributes :module_stream_id, :if => lambda { |rule| rule.respond_to?(:module_stream_id) && !rule.module_stream_id.blank? }
18
+ if @resource&.try(:module_stream)
19
+ node :module_stream do |rule|
20
+ {
21
+ :module_stream_id => rule.module_stream.id,
22
+ :module_stream_name => rule.module_stream.name,
23
+ :module_stream_stream => rule.module_stream.stream
24
+ }
25
+ end
26
+ end
18
27
  extends 'katello/api/v2/common/timestamps'
@@ -1,6 +1,7 @@
1
1
  object @resource
2
2
 
3
3
  attributes :id, :schema_version, :digest, :manifest_type
4
+ attributes :annotations, :labels, :is_bootable, :is_flatpak
4
5
 
5
6
  child :docker_tags => :tags do
6
7
  attributes :associated_meta_tag_identifier => :id
@@ -1,3 +1,7 @@
1
1
  child :host_collections => :host_collections do
2
- attributes :id, :name, :description, :max_hosts, :unlimited_hosts, :total_hosts
2
+ attributes :id, :name, :description, :max_hosts, :unlimited_hosts
3
+
4
+ node :total_hosts do |host_collection|
5
+ host_collection.total_hosts(cached: true)
6
+ end
3
7
  end
@@ -3,10 +3,18 @@ object @organization
3
3
  extends "api/v2/taxonomies/show"
4
4
 
5
5
  attributes :task_id, :label, :redhat_repository_url
6
-
6
+ attributes :manifest_expiration_date, :manifest_expire_days_remaining
7
7
  attributes :system_purposes, :system_purposes
8
8
  attributes :service_levels, :service_level
9
9
 
10
+ node :manifest_expiring_soon do |org|
11
+ org.manifest_expiring_soon?
12
+ end
13
+
14
+ node :manifest_expired do |org|
15
+ org.manifest_expired?
16
+ end
17
+
10
18
  node :simple_content_access do |org|
11
19
  org.simple_content_access?
12
20
  end
@@ -43,7 +43,7 @@
43
43
  <% end %>
44
44
  <% if host.operatingsystem_name&.match(/Debian|Ubuntu/) %>
45
45
  <% if Setting["host_details_ui"] %>
46
- <a href="/new/hosts/<%= host.name %>#/Content/packages?status=Upgradable">
46
+ <a href="/new/hosts/<%= host.name %>#/Content/debs?status=Upgradable">
47
47
  <% else %>
48
48
  <a href="/content_hosts/<%= host.id %>/debs/applicable">
49
49
  <% end %>
@@ -8,7 +8,7 @@
8
8
 
9
9
  <% if edit_action? && !using_hostgroups_page? && !using_discovered_hosts_page? %>
10
10
  <div style="margin-left: 270px">
11
- <%= link_to _("Change content source"), "/change_host_content_source?host_id=#{@host.id}" %>
11
+ <%= link_to _("Change content source"), "/change_host_content_source?fromPage=hostEdit&host_id=#{@host.id}&initialContentSourceId=#{@host.content_source_id}" %>
12
12
  </div>
13
13
  <% end %>
14
14
 
@@ -16,19 +16,29 @@
16
16
  %>
17
17
 
18
18
  <div class="clear-fix" id="media_selection_section">
19
- <div class="form-group ">
20
- <label class="col-md-2 control-label" for="use_install_media"><%= _('Media Selection') %></label>
19
+ <div class="form-group">
20
+ <label class="col-md-2 control-label" for="use_install_media">
21
+ <%= _('Media Selection') %>
22
+ </label>
21
23
  <div class="col-md-4">
22
24
  <label class="radio-inline">
23
- <input data-media-selector="true" id="host_use_synced_content" name="media_selector" value="synced_content" type="radio" <%= synced_content_radio %> <%= synced_content_disabled %>/> <%= _('Synced Content') %>
25
+ <input data-media-selector="true" id="host_use_synced_content" name="media_selector" value="synced_content" type="radio"
26
+ <%= synced_content_radio %>
27
+ <%= synced_content_disabled %>
28
+ />
29
+ <%= _('Synced Content') %>
24
30
  </label>
31
+ <%= popover("", _("To enable the synced content option, this host must use a content source, content view, and lifecycle environment which contain synced kickstart repositories for the selected architecture and operating system.")) %>
25
32
  <label class="radio-inline">
26
- <input data-media-selector="true" id="host_use_install_media" name="media_selector" value="install_media" type="radio" <%= install_media_radio %> <%= install_media_disabled %>/> <%= _('All Media') %>
33
+ <input data-media-selector="true" id="host_use_install_media" name="media_selector" value="install_media" type="radio"
34
+ <%= install_media_radio %>
35
+ <%= install_media_disabled %>
36
+ />
37
+ <%= _('All Media') %>
27
38
  </label>
28
39
  <span class="help-block">
29
40
  <%= _("Select the installation media that will be used to provision this host. Choose 'Synced Content' for Synced Kickstart Repositories or 'All Media' for other media.") %>
30
41
  </span>
31
42
  </div>
32
- <span class="help-block help-inline"></span>
33
43
  </div>
34
44
  </div>
@@ -1,31 +1,3 @@
1
-
2
- <%= javascript "katello/hosts/activation_key_edit" %>
3
-
4
-
5
1
  <div class="tab-pane" id="activation_keys">
6
- <div id="ak-load-error" class="alert alert-danger" style="display: none;">
7
- <%= _("There was a problem retrieving Activation Key data from the server.") %>
8
- </div>
9
-
10
- <%= field(f, _("Activation Keys"),
11
- :help_inline => _("The value will be available in templates as @host.params['#{kt_ak_label}']")) do
12
- react_component('TypeAheadSelect', { id: 'kt_activation_keys', multiple: true, allowNew: true })
13
- end %>
14
-
15
- <div class="alert alert-info">
16
- <p><b><%= _('Subscriptions information based on selected activation keys:') %></b></p>
17
- <ul id="ak-subscriptions-info"></ul>
18
-
19
- <div id="ak-subscriptions-spinner" style="display: none">
20
- <%= image_tag "spinner.gif" %>
21
- </div>
22
-
23
- <p><%= _('Activation keys and subscriptions can be managed') %>
24
- <b><a href="/activation_keys" target="_blank"> <%= _('here.') %></a></b>
25
- </p>
26
- <p translate>
27
- Activation keys may be used during <a href="/hosts/register">system registration.</a>
28
- </p>
29
- <p><a href="" id="ak_refresh_subscriptions"><%= _('Reload data') %></a></p>
30
- </div>
2
+ <%= react_component('ActivationKeysSearch')%>
31
3
  </div>
@@ -10,16 +10,12 @@ Katello::Engine.routes.draw do
10
10
  match '/v2/token' => 'registry_proxies#token', :via => :get
11
11
  match '/v2/token' => 'registry_proxies#token', :via => :post
12
12
  match '/v2/*repository/manifests/:tag' => 'registry_proxies#pull_manifest', :via => :get
13
- # Push-related routes are disabled until there is support for pushing to Pulp 3.
14
- # match '/v2/*repository/manifests/:tag' => 'registry_proxies#push_manifest', :via => :put
13
+ match '/v2/*repository/manifests/:tag' => 'registry_proxies#push_manifest', :via => :put
15
14
  match '/v2/*repository/blobs/:digest' => 'registry_proxies#pull_blob', :via => :get
16
15
  match '/v2/*repository/blobs/:digest' => 'registry_proxies#check_blob', :via => :head
17
- # match '/v2/*repository/blobs/uploads' => 'registry_proxies#start_upload_blob', :via => :post
18
- # match '/v2/*repository/blobs/uploads/:uuid' => 'registry_proxies#chunk_upload_blob', :via => :post
19
- # match '/v2/*repository/blobs/uploads/:uuid' => 'registry_proxies#finish_upload_blob', :via => :put
20
- # match '/v2/*repository/blobs/uploads/:uuid' => 'registry_proxies#upload_blob', :via => :patch
21
- # match '/v2/*repository/blobs/uploads/:uuid' => 'registry_proxies#status_upload_blob', :via => :get
22
- # match '/v2/*repository/blobs/uploads/:uuid' => 'registry_proxies#cancel_upload_blob', :via => :delete
16
+ match '/v2/*repository/blobs/uploads' => 'registry_proxies#start_upload_blob', :via => :post
17
+ match '/v2/*repository/blobs/uploads/:uuid' => 'registry_proxies#finish_upload_blob', :via => :put
18
+ match '/v2/*repository/blobs/uploads/:uuid' => 'registry_proxies#upload_blob', :via => :patch
23
19
  match '/v2/_catalog' => 'registry_proxies#catalog', :via => :get
24
20
  match '/v2/*repository/tags/list' => 'registry_proxies#tags_list', :via => :get
25
21
  match '/v2' => 'registry_proxies#ping', :via => :get
@@ -29,6 +29,7 @@ Katello::Engine.routes.draw do
29
29
  get :sync, :action => :sync_status
30
30
  delete :sync, :action => :cancel_sync
31
31
  post :reclaim_space
32
+ post :verify_checksum
32
33
  post '/lifecycle_environments' => 'capsule_content#add_lifecycle_environment'
33
34
  delete '/lifecycle_environments/:environment_id' => 'capsule_content#remove_lifecycle_environment'
34
35
  end
@@ -149,6 +150,7 @@ Katello::Engine.routes.draw do
149
150
  post :promote
150
151
  post :export
151
152
  put :republish_repositories
153
+ post :verify_checksum
152
154
  end
153
155
  collection do
154
156
  get :auto_complete_search
@@ -0,0 +1,8 @@
1
+ class AddFieldsToKatelloDockerManifest < ActiveRecord::Migration[6.1]
2
+ def change
3
+ add_column :katello_docker_manifests, :annotations, :jsonb, default: {}
4
+ add_column :katello_docker_manifests, :labels, :jsonb, default: {}
5
+ add_column :katello_docker_manifests, :is_bootable, :boolean, default: false
6
+ add_column :katello_docker_manifests, :is_flatpak, :boolean, default: false
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ class ChangeKatelloRepositoryRpmsIdSeqToBigInt < ActiveRecord::Migration[6.1]
2
+ def up
3
+ execute 'ALTER SEQUENCE katello_repository_rpms_id_seq AS bigint;'
4
+ end
5
+
6
+ def down
7
+ execute 'ALTER SEQUENCE katello_repository_rpms_id_seq AS integer;'
8
+ end
9
+ end
@@ -25,6 +25,12 @@ blueprints = [
25
25
  message: N_('%{expiring_subs} subscriptions in %{subject} are going to expire in less than %{days} days. Please renew them before they expire to guarantee your hosts will continue receiving content.'),
26
26
  level: 'warning'
27
27
  },
28
+ {
29
+ group: N_('Subscriptions'),
30
+ name: 'manifest_expire_soon_warning',
31
+ message: N_('Manifest in organization %{subject} has an identity certificate that will expire in %{days_remaining} days, on %{manifest_expire_date}. To extend the expiration date, please refresh your manifest.'),
32
+ level: 'info'
33
+ },
28
34
  {
29
35
  group: N_('Subscriptions'),
30
36
  name: 'manifest_expired_warning',
@@ -23,7 +23,7 @@ angular.module('Bastion.activation-keys').controller('ActivationKeyRepositorySet
23
23
 
24
24
  params = {
25
25
  'activation_key_id': $scope.$stateParams.activationKeyId,
26
- 'content_access_mode_all': $scope.simpleContentAccessEnabled,
26
+ 'content_access_mode_all': true,
27
27
  'content_access_mode_env': true,
28
28
  'sort_order': 'ASC',
29
29
  'paged': true
@@ -39,7 +39,7 @@ angular.module('Bastion.activation-keys').controller('ActivationKeyRepositorySet
39
39
  $scope.repositoryType = {};
40
40
 
41
41
  $scope.contentAccessModes = {
42
- contentAccessModeAll: $scope.simpleContentAccessEnabled,
42
+ contentAccessModeAll: true,
43
43
  contentAccessModeEnv: true
44
44
  };
45
45
 
@@ -53,7 +53,7 @@ angular.module('Bastion.activation-keys').controller('ActivationKeyRepositorySet
53
53
 
54
54
  $scope.toggleFilters = function () {
55
55
  $scope.nutupane.table.params['content_access_mode_env'] = $scope.contentAccessModes.contentAccessModeEnv;
56
- $scope.nutupane.table.params['content_access_mode_all'] = $scope.contentAccessModes.contentAccessModeAll || $scope.simpleContentAccessEnabled;
56
+ $scope.nutupane.table.params['content_access_mode_all'] = true;
57
57
  $scope.nutupane.refresh();
58
58
  };
59
59
 
@@ -35,7 +35,8 @@
35
35
  class="form-control"
36
36
  style="font-family: monospace"
37
37
  rows="15"
38
- placeholder="{{ 'Paste contents of Content Credential' | translate }}">
38
+ placeholder="{{ 'Paste contents of Content Credential' | translate }}"
39
+ required>
39
40
  </textarea>
40
41
  </div>
41
42
 
@@ -20,7 +20,7 @@ angular.module('Bastion.content-hosts').controller('ContentHostRepositorySetsCon
20
20
 
21
21
  params = {
22
22
  'host_id': $scope.$stateParams.hostId,
23
- 'content_access_mode_all': $scope.simpleContentAccessEnabled,
23
+ 'content_access_mode_all': true,
24
24
  'sort_order': 'ASC',
25
25
  'paged': true
26
26
  };
@@ -33,11 +33,11 @@ angular.module('Bastion.content-hosts').controller('ContentHostRepositorySetsCon
33
33
  $scope.nutupane.primaryOnly = true;
34
34
 
35
35
  $scope.contentAccessModes = {
36
- contentAccessModeAll: $scope.simpleContentAccessEnabled,
36
+ contentAccessModeAll: true,
37
37
  contentAccessModeEnv: false
38
38
  };
39
39
  $scope.toggleFilters = function () {
40
- $scope.nutupane.table.params['content_access_mode_all'] = $scope.contentAccessModes.contentAccessModeAll || $scope.simpleContentAccessEnabled;
40
+ $scope.nutupane.table.params['content_access_mode_all'] = true;
41
41
  $scope.nutupane.table.params['content_access_mode_env'] = $scope.contentAccessModes.contentAccessModeEnv;
42
42
  $scope.nutupane.refresh();
43
43
  };
@@ -3973,7 +3973,6 @@ msgstr ""
3973
3973
  #: app/assets/javascripts/bastion_katello/content-hosts/content/views/content-host-module-streams.html
3974
3974
  #: app/assets/javascripts/bastion_katello/content-hosts/details/views/content-host-provisioning-info.html
3975
3975
  #: app/assets/javascripts/bastion_katello/content-hosts/details/views/content-host-repository-sets.html
3976
- #: app/assets/javascripts/bastion_katello/content-hosts/details/views/content-host-subscriptions.html
3977
3976
  #: app/assets/javascripts/bastion_katello/tasks/views/tasks-table.html
3978
3977
  #: app/assets/javascripts/bastion_katello/tasks/views/user-tasks-table.html
3979
3978
  msgid "Status"
@@ -3995,8 +3994,6 @@ msgid "Subscription Management"
3995
3994
  msgstr ""
3996
3995
 
3997
3996
  #: app/assets/javascripts/bastion_katello/activation-keys/details/views/activation-key-associations-content-hosts.html
3998
- #: app/assets/javascripts/bastion_katello/content-hosts/details/views/content-host-info.html
3999
- #: app/assets/javascripts/bastion_katello/content-hosts/views/content-hosts.html
4000
3997
  msgid "Subscription Status"
4001
3998
  msgstr ""
4002
3999
 
@@ -4178,10 +4175,6 @@ msgstr ""
4178
4175
  msgid "System Purpose Management"
4179
4176
  msgstr ""
4180
4177
 
4181
- #: app/assets/javascripts/bastion_katello/content-hosts/details/views/content-host-info.html
4182
- msgid "System Purpose Status"
4183
- msgstr ""
4184
-
4185
4178
  #: app/assets/javascripts/bastion_katello/products/details/repositories/details/views/repository-manage-docker-manifest-lists.html
4186
4179
  #: app/assets/javascripts/bastion_katello/products/details/repositories/details/views/repository-manage-docker-manifests.html
4187
4180
  msgid "Tags"
@@ -4398,14 +4391,6 @@ msgid ""
4398
4391
  " Change the setting \"Delete Host upon Unregister\" to false on the <a href=\"/settings\">settings page</a> to prevent this."
4399
4392
  msgstr ""
4400
4393
 
4401
- #: app/assets/javascripts/bastion_katello/subscriptions/views/content-access-mode-banner.html
4402
- msgid "This organization has Simple Content Access enabled. Hosts are not required to have subscriptions attached to access repositories."
4403
- msgstr ""
4404
-
4405
- #: app/assets/javascripts/bastion_katello/subscriptions/views/content-access-mode-banner.html
4406
- msgid "This organization is not using <a target=\"_blank\" href=\"https://access.redhat.com/articles/simple-content-access\">Simple Content Access.</a> Entitlement-based subscription management is deprecated and will be removed in Katello 4.12."
4407
- msgstr ""
4408
-
4409
4394
  #: app/assets/javascripts/bastion_katello/content-hosts/bulk/views/content-hosts-bulk-errata-modal.html
4410
4395
  #: app/assets/javascripts/bastion_katello/content-hosts/content/views/content-host-errata.html
4411
4396
  #: app/assets/javascripts/bastion_katello/content-hosts/content/views/errata-details.html
@@ -189,12 +189,14 @@
189
189
  type="{{ repository.upstream_password.length < 1 ? 'text': 'password' }}"
190
190
  autocomplete="{{ (repository.upstream_username==null || repository.upstream_username=='') ? 'new-password' : '' }}"
191
191
  ng-model="repository.upstream_password"/>
192
- <div translate>Upstream Authentication Token</div>
193
- <input id="upstream_authentication_token"
194
- name="upstream_authentication_token"
195
- type="{{ repository.upstream_authentication_token.length < 1 ? 'text': 'password' }}"
196
- autocomplete="off"
197
- ng-model="repository.upstream_authentication_token"/>
192
+ <span ng-show="repository.content_type === 'yum'">
193
+ <div translate>Upstream Authentication Token</div>
194
+ <input id="upstream_authentication_token"
195
+ name="upstream_authentication_token"
196
+ type="{{ repository.upstream_authentication_token.length < 1 ? 'text': 'password' }}"
197
+ autocomplete="off"
198
+ ng-model="repository.upstream_authentication_token"/>
199
+ </span>
198
200
  </dd>
199
201
  </span>
200
202