foreman_scc_manager 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +38 -0
- data/Rakefile +47 -0
- data/app/assets/javascripts/foreman_scc_manager/scc_accounts.js.coffee +46 -0
- data/app/controllers/scc_accounts_controller.rb +118 -0
- data/app/controllers/scc_products_controller.rb +24 -0
- data/app/helpers/scc_accounts_helper.rb +2 -0
- data/app/helpers/scc_product_helper.rb +2 -0
- data/app/lib/actions/scc_manager/subscribe_product.rb +72 -0
- data/app/lib/actions/scc_manager/sync.rb +36 -0
- data/app/lib/actions/scc_manager/sync_products.rb +37 -0
- data/app/lib/actions/scc_manager/sync_repositories.rb +36 -0
- data/app/lib/scc_manager.rb +37 -0
- data/app/models/scc_account.rb +100 -0
- data/app/models/scc_extending.rb +4 -0
- data/app/models/scc_product.rb +46 -0
- data/app/models/scc_repository.rb +25 -0
- data/app/views/scc_accounts/_form.html.erb +36 -0
- data/app/views/scc_accounts/edit.html.erb +3 -0
- data/app/views/scc_accounts/index.html.erb +38 -0
- data/app/views/scc_accounts/new.html.erb +3 -0
- data/app/views/scc_accounts/show.html.erb +41 -0
- data/config/routes.rb +17 -0
- data/db/migrate/20170221100619_create_scc_accounts.rb +11 -0
- data/db/migrate/20170227103408_create_scc_products.rb +16 -0
- data/db/migrate/20170301131641_create_scc_repositories.rb +18 -0
- data/db/migrate/20170301141330_create_scc_products_scc_repositories_join_table.rb +8 -0
- data/db/migrate/20170301163451_add_product_type_to_scc_product.rb +5 -0
- data/db/migrate/20170302082912_remove_repositories_from_scc_products.rb +5 -0
- data/db/migrate/20170302121542_create_scc_extendings.rb +12 -0
- data/db/migrate/20170303085304_add_organization_to_scc_account.rb +22 -0
- data/db/migrate/20170303131704_add_product_id_to_scc_product.rb +6 -0
- data/db/migrate/20170307092057_add_synced_to_scc_account.rb +5 -0
- data/db/migrate/20170418132648_add_name_to_scc_account.rb +5 -0
- data/db/migrate/20170505063726_add_sync_status_to_scc_account.rb +5 -0
- data/lib/foreman_scc_manager.rb +4 -0
- data/lib/foreman_scc_manager/engine.rb +80 -0
- data/lib/foreman_scc_manager/version.rb +3 -0
- data/lib/tasks/foreman_scc_manager_tasks.rake +47 -0
- data/locale/Makefile +60 -0
- data/locale/action_names.rb +61 -0
- data/locale/de/LC_MESSAGES/foreman_scc_manager.mo +0 -0
- data/locale/de/foreman_scc_manager.po +265 -0
- data/locale/en/LC_MESSAGES/foreman_scc_manager.mo +0 -0
- data/locale/en/foreman_scc_manager.po +265 -0
- data/locale/foreman_scc_manager.pot +345 -0
- data/locale/gemspec.rb +2 -0
- data/test/factories/foreman_scc_manager_factories.rb +5 -0
- data/test/test_plugin_helper.rb +6 -0
- data/test/unit/foreman_scc_manager_test.rb +11 -0
- metadata +154 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module SccManager
|
2
|
+
# adapted from https://github.com/SUSE/connect
|
3
|
+
def self.get_scc_data(base_url, rest_url, login, password)
|
4
|
+
if SETTINGS[:katello][:cdn_proxy] && SETTINGS[:katello][:cdn_proxy][:host]
|
5
|
+
proxy_config = SETTINGS[:katello][:cdn_proxy]
|
6
|
+
uri = URI('')
|
7
|
+
|
8
|
+
uri.scheme = URI.parse(proxy_config[:host]).scheme
|
9
|
+
uri.host = URI.parse(proxy_config[:host]).host
|
10
|
+
uri.port = proxy_config[:port].try(:to_s)
|
11
|
+
uri.user = proxy_config[:user].try(:to_s)
|
12
|
+
uri.password = proxy_config[:password].try(:to_s)
|
13
|
+
|
14
|
+
RestClient.proxy = uri.to_s
|
15
|
+
end
|
16
|
+
|
17
|
+
url = base_url + rest_url
|
18
|
+
auth_header = { Authorization: 'Basic ' + Base64.encode64("#{login}:#{password}").chomp,
|
19
|
+
Accept: 'application/vnd.scc.suse.com.v4+json' }
|
20
|
+
results = []
|
21
|
+
loop do
|
22
|
+
response = RestClient.get url, auth_header
|
23
|
+
raise 'Connection to SUSE costomer center failed.' unless response.code == 200
|
24
|
+
links = (response.headers[:link] || '').split(', ').map do |link|
|
25
|
+
href, rel = /<(.*?)>; rel="(\w+)"/.match(link).captures
|
26
|
+
[rel.to_sym, href]
|
27
|
+
end
|
28
|
+
links = Hash[*links.flatten]
|
29
|
+
results += JSON.parse response
|
30
|
+
url = links[:next]
|
31
|
+
break unless url
|
32
|
+
end
|
33
|
+
results
|
34
|
+
ensure
|
35
|
+
RestClient.proxy = ''
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
class SccAccount < ActiveRecord::Base
|
2
|
+
include Authorizable
|
3
|
+
include Encryptable
|
4
|
+
include ForemanTasks::Concerns::ActionSubject
|
5
|
+
encrypts :password
|
6
|
+
|
7
|
+
SYNC_STATI = [ nil, 'running', 'successful', 'failed' ]
|
8
|
+
|
9
|
+
self.include_root_in_json = false
|
10
|
+
|
11
|
+
belongs_to :organization
|
12
|
+
has_many :scc_products, dependent: :destroy
|
13
|
+
has_many :scc_repositories, dependent: :destroy
|
14
|
+
|
15
|
+
validates_lengths_from_database
|
16
|
+
validates :name, presence: true
|
17
|
+
validates :organization, presence: true
|
18
|
+
validates :login, presence: true
|
19
|
+
validates :password, presence: true
|
20
|
+
validates :base_url, presence: true
|
21
|
+
validates_inclusion_of :sync_status, in: SYNC_STATI
|
22
|
+
|
23
|
+
default_scope -> { order(:login) }
|
24
|
+
|
25
|
+
scoped_search on: :login, complete_value: true
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
name
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_sync_status
|
32
|
+
if sync_status == nil
|
33
|
+
return _('never synced')
|
34
|
+
elsif sync_status == 'running'
|
35
|
+
return _('sync in progress')
|
36
|
+
elsif sync_status == 'successful'
|
37
|
+
return synced
|
38
|
+
elsif sync_status == 'failed'
|
39
|
+
return _('last sync failed')
|
40
|
+
end
|
41
|
+
''
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_connection
|
45
|
+
begin
|
46
|
+
SccManager::get_scc_data(base_url, '/connect/organizations/subscriptions', login, password)
|
47
|
+
true
|
48
|
+
rescue
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def update_scc_repositories(upstream_repositories)
|
54
|
+
upstream_repo_ids = []
|
55
|
+
SccProduct.transaction do
|
56
|
+
# import repositories
|
57
|
+
upstream_repositories.each do |ur|
|
58
|
+
cached_repository = scc_repositories.find_or_initialize_by(scc_id: ur['id'])
|
59
|
+
cached_repository.name = ur['name']
|
60
|
+
cached_repository.distro_target = ur['distro_target']
|
61
|
+
cached_repository.description = ur['description']
|
62
|
+
cached_repository.url, cached_repository.token = ur['url'].split('?')
|
63
|
+
cached_repository.enabled = ur['enabled']
|
64
|
+
cached_repository.autorefresh = ur['autorefresh']
|
65
|
+
cached_repository.installer_updates = ur['installer_updates']
|
66
|
+
cached_repository.save!
|
67
|
+
upstream_repo_ids << cached_repository.id
|
68
|
+
end
|
69
|
+
# delete repositories beeing removed upstream
|
70
|
+
scc_repositories.where(id: scc_repository_ids - upstream_repo_ids).destroy_all.count
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def update_scc_products(upstream_products)
|
75
|
+
upstream_product_ids = []
|
76
|
+
SccProduct.transaction do
|
77
|
+
# import products
|
78
|
+
upstream_products.each do |up|
|
79
|
+
cached_product = scc_products.find_or_initialize_by(scc_id: up['id'])
|
80
|
+
cached_product.name = up['name']
|
81
|
+
cached_product.version = up['version']
|
82
|
+
cached_product.arch = up['arch']
|
83
|
+
cached_product.description = up['description']
|
84
|
+
cached_product.friendly_name = up['friendly_name']
|
85
|
+
cached_product.product_type = up['product_type']
|
86
|
+
cached_product.scc_repositories =
|
87
|
+
scc_repositories.where(scc_id: up['repositories'].map { |repo| repo['id'] })
|
88
|
+
cached_product.save!
|
89
|
+
upstream_product_ids << cached_product.id
|
90
|
+
end
|
91
|
+
# delete products beeing removed upstream
|
92
|
+
scc_products.where(id: scc_product_ids - upstream_product_ids).destroy_all.count
|
93
|
+
# rewire product to product relationships
|
94
|
+
upstream_products.each do |up|
|
95
|
+
extensions = scc_products.where(scc_id: up['extensions'].map { |ext| ext['id'] })
|
96
|
+
scc_products.find_by!(scc_id: up['id']).update!(scc_extensions: extensions)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
class SccProduct < ActiveRecord::Base
|
2
|
+
include Authorizable
|
3
|
+
include ForemanTasks::Concerns::ActionSubject
|
4
|
+
|
5
|
+
self.include_root_in_json = false
|
6
|
+
|
7
|
+
belongs_to :scc_account
|
8
|
+
belongs_to :product, class_name: 'Katello::Product'
|
9
|
+
has_one :organization, through: :scc_account
|
10
|
+
has_and_belongs_to_many :scc_repositories
|
11
|
+
has_many :scc_extendings, dependent: :destroy
|
12
|
+
has_many :scc_extensions, through: :scc_extendings
|
13
|
+
has_many :inverse_scc_extendings,
|
14
|
+
dependent: :destroy,
|
15
|
+
class_name: :SccExtending,
|
16
|
+
foreign_key: :scc_extension_id
|
17
|
+
has_many :inverse_scc_extensions, through: :inverse_scc_extendings, source: :scc_product
|
18
|
+
|
19
|
+
default_scope -> { order(:name) }
|
20
|
+
scoped_search on: :name, complete_value: true
|
21
|
+
|
22
|
+
def uniq_name
|
23
|
+
return "#{scc_id} " + friendly_name;
|
24
|
+
end
|
25
|
+
|
26
|
+
def subscribe
|
27
|
+
raise 'Product already subscribed!' if product
|
28
|
+
new_product = Katello::Product.new
|
29
|
+
new_product.name = uniq_name
|
30
|
+
new_product.description = description
|
31
|
+
ForemanTasks.sync_task(::Actions::Katello::Product::Create, new_product, scc_account.organization)
|
32
|
+
new_product.reload
|
33
|
+
scc_repositories.each do |repo|
|
34
|
+
uniq_repo_name = uniq_name + ' ' + repo.description
|
35
|
+
label = Katello::Util::Model.labelize(uniq_repo_name)
|
36
|
+
unprotected = true
|
37
|
+
gpg_key = new_product.gpg_key
|
38
|
+
new_repo = new_product.add_repo(label, uniq_repo_name, repo.full_url, 'yum', unprotected, gpg_key)
|
39
|
+
new_repo.arch = arch || 'noarch'
|
40
|
+
new_repo.mirror_on_sync = true
|
41
|
+
new_repo.verify_ssl_on_sync = true
|
42
|
+
ForemanTasks.sync_task(::Actions::Katello::Repository::Create, new_repo, false, false)
|
43
|
+
end
|
44
|
+
update!(product: new_product)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class SccRepository < ActiveRecord::Base
|
2
|
+
after_commit :token_changed_callback
|
3
|
+
|
4
|
+
self.include_root_in_json = false
|
5
|
+
|
6
|
+
belongs_to :scc_account
|
7
|
+
has_one :organization, through: :scc_account
|
8
|
+
has_and_belongs_to_many :scc_products
|
9
|
+
|
10
|
+
def full_url
|
11
|
+
token.blank? ? url : url + '?' + token
|
12
|
+
end
|
13
|
+
|
14
|
+
def token_changed_callback
|
15
|
+
User.current = User.anonymous_admin unless User.current
|
16
|
+
scc_products.where.not(product: nil).find_each do |sp|
|
17
|
+
reponame = sp.friendly_name + ' ' + description
|
18
|
+
repository = sp.product.repositories.find_by(name: reponame)
|
19
|
+
unless repository.url == full_url
|
20
|
+
::Foreman::Logging.logger('foreman_scc_manager').info "Update URL-token for repository '#{reponame}'."
|
21
|
+
ForemanTasks.async_task(::Actions::Katello::Repository::Update, repository, url: full_url)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
<% javascript 'foreman_scc_manager/scc_accounts' %>
|
2
|
+
|
3
|
+
<%= form_for(@scc_account) do |f| %>
|
4
|
+
<%= base_errors_for @scc_account %>
|
5
|
+
<ul class="nav nav-tabs" data-tabs="tabs">
|
6
|
+
<li class="active"><a href="#primary" data-toggle="tab"><%= _("SUSE Customer Center account") %></a></li>
|
7
|
+
</ul>
|
8
|
+
|
9
|
+
<div class="tab-content">
|
10
|
+
<div class="tab-pane active" id="primary">
|
11
|
+
<div>
|
12
|
+
<%= text_f f, :name %>
|
13
|
+
<%= text_f f, :login %>
|
14
|
+
<%= password_f f, :password %>
|
15
|
+
<%= text_f f, :base_url, label: _('Base URL') %>
|
16
|
+
<div class='clearfix'>
|
17
|
+
<div class='form-group'>
|
18
|
+
<div class='col-md-2'></div>
|
19
|
+
<div class='col-md-4'>
|
20
|
+
<%= spinner_button_f(f, _('Test Connection'), '',
|
21
|
+
id: 'test_scc_connection_btn',
|
22
|
+
spinner_id: 'test_scc_connection_indicator',
|
23
|
+
class: 'btn-default',
|
24
|
+
'data-url': test_connection_scc_accounts_path(scc_account_id: @scc_account)) %>
|
25
|
+
</div>
|
26
|
+
<div class='col-md-2'>
|
27
|
+
<span id='connection_test_result'></span>
|
28
|
+
</div>
|
29
|
+
</div>
|
30
|
+
</div>
|
31
|
+
<%= f.hidden_field :organization_id %>
|
32
|
+
<%= submit_or_cancel f %>
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
</div>
|
36
|
+
<% end %>
|
@@ -0,0 +1,38 @@
|
|
1
|
+
<% javascript 'foreman_scc_manager/scc_accounts' %>
|
2
|
+
<% title _("SUSE subscriptions") %>
|
3
|
+
<% title_actions new_link(_("Add SCC account")) %>
|
4
|
+
|
5
|
+
<table class="<%= table_css_classes 'table-two-pane table_fixed' %>">
|
6
|
+
<thead>
|
7
|
+
<tr>
|
8
|
+
<th class="col-md-4"><%= sort :name %></th>
|
9
|
+
<th class="col-md-3"><%= _("Products") %></th>
|
10
|
+
<th class="col-md-3"><%= _("Last synced") %></th>
|
11
|
+
<th class="col-md-2"><%= _("Actions") %></th>
|
12
|
+
</tr>
|
13
|
+
</thead>
|
14
|
+
<tbody>
|
15
|
+
<% @scc_accounts.each do |scc_account| %>
|
16
|
+
<tr>
|
17
|
+
<td class="display-two-pane ellipsis">
|
18
|
+
<%= link_to_if_authorized(scc_account.name, hash_for_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer)) %>
|
19
|
+
<%= link_to_if_authorized('', hash_for_edit_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer), {visible: false, class: 'edit_deferree'}) %>
|
20
|
+
</td>
|
21
|
+
<td><%= scc_account.scc_products.count.to_s %></td>
|
22
|
+
<td><%= scc_account.get_sync_status %></td>
|
23
|
+
<td>
|
24
|
+
<%= action_buttons(
|
25
|
+
display_link_if_authorized(_("Sync"), hash_for_sync_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer),
|
26
|
+
:method => :put),
|
27
|
+
display_link_if_authorized(_("Edit"), hash_for_edit_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer),
|
28
|
+
class: 'edit_deferrer'),
|
29
|
+
display_delete_if_authorized(hash_for_scc_account_path(:id => scc_account).merge(:auth_object => scc_account, :authorizer => authorizer),
|
30
|
+
:data => { :confirm => _("Delete %s?") % scc_account.to_s })
|
31
|
+
) %>
|
32
|
+
</td>
|
33
|
+
</tr>
|
34
|
+
<% end %>
|
35
|
+
</tbody>
|
36
|
+
</table>
|
37
|
+
|
38
|
+
<%= will_paginate_with_info @scc_accounts %>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<% javascript 'foreman_scc_manager/scc_accounts' %>
|
2
|
+
<%= form_for([:bulk_subscribe, @scc_account], method: :put) do |f| %>
|
3
|
+
<% def render_list_node(f, scc_product, parent_id = nil) %>
|
4
|
+
<li>
|
5
|
+
<span class="scc_product_checkbox" id="<%= "scc_product_span_#{scc_product.id}" %>" <%= "data-parent=scc_product_span_#{parent_id}" if parent_id %>>
|
6
|
+
<% if scc_product.product %>
|
7
|
+
<%= check_box_tag("scc_account[scc_unsubscribe_product_ids][]", scc_product.id, true, disabled: true) %>
|
8
|
+
<% else %>
|
9
|
+
<%= check_box_tag("scc_account[scc_subscribe_product_ids][]", scc_product.id, false) %>
|
10
|
+
<% end %>
|
11
|
+
</span>
|
12
|
+
<%= scc_product.friendly_name %>
|
13
|
+
<% if scc_product.scc_extensions.any? %>
|
14
|
+
<ul>
|
15
|
+
<% scc_product.scc_extensions.order(:friendly_name).each do |scc_extension| %>
|
16
|
+
<% render_list_node(f, scc_extension, scc_product.id) %>
|
17
|
+
<% end %>
|
18
|
+
</ul>
|
19
|
+
<% end %>
|
20
|
+
</li>
|
21
|
+
<% end %>
|
22
|
+
<%= f.hidden_field :prevent_missing, value: 1 %>
|
23
|
+
<ul class="nav nav-tabs" data-tabs="tabs">
|
24
|
+
<li class="active"><a href="#primary" data-toggle="tab"><%= _("SUSE Customer Center") %></a></li>
|
25
|
+
</ul>
|
26
|
+
|
27
|
+
<div class="tab-content">
|
28
|
+
<div class="tab-pane active pre-scrollable" id="primary">
|
29
|
+
<ul>
|
30
|
+
<% if @scc_account.synced %>
|
31
|
+
<% @scc_account.scc_products.where(product_type: 'base').order(:friendly_name).each do |scc_product| %>
|
32
|
+
<% render_list_node(f, scc_product) %>
|
33
|
+
<% end %>
|
34
|
+
<% else %>
|
35
|
+
<%= _('Please sync your SUSE subscriptions first.') %>
|
36
|
+
<% end %>
|
37
|
+
</ul>
|
38
|
+
</div>
|
39
|
+
<%= submit_or_cancel f %>
|
40
|
+
</div>
|
41
|
+
<% end %>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Rails.application.routes.draw do
|
2
|
+
resources :scc_accounts do
|
3
|
+
collection do
|
4
|
+
put 'test_connection'
|
5
|
+
end
|
6
|
+
member do
|
7
|
+
put 'sync'
|
8
|
+
put 'bulk_subscribe'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
resources :scc_products, only: [:index, :show] do
|
12
|
+
member do
|
13
|
+
put 'subscribe'
|
14
|
+
put 'unsubscribe'
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateSccAccounts < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :scc_accounts do |t|
|
4
|
+
t.string :login, limit: 255
|
5
|
+
t.string :password, limit: 255
|
6
|
+
t.string :base_url, limit: 255, default: 'https://scc.suse.com'
|
7
|
+
|
8
|
+
t.timestamps null: false
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
class CreateSccProducts < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :scc_products do |t|
|
4
|
+
t.references :scc_account, index: true, foreign_key: true
|
5
|
+
t.integer :scc_id, unique: true
|
6
|
+
t.string :name, limit: 255
|
7
|
+
t.string :version, limit: 63
|
8
|
+
t.string :arch, limit: 31
|
9
|
+
t.string :friendly_name, limit: 255
|
10
|
+
t.string :description
|
11
|
+
t.string :repositories
|
12
|
+
|
13
|
+
t.timestamps null: false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class CreateSccRepositories < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :scc_repositories do |t|
|
4
|
+
t.references :scc_account, index: true, foreign_key: true
|
5
|
+
t.integer :scc_id, unique: true
|
6
|
+
t.string :name, limit: 255
|
7
|
+
t.string :distro_target, limit: 255
|
8
|
+
t.string :description, limit: 255
|
9
|
+
t.string :url, limit: 255
|
10
|
+
t.string :token, limit: 255
|
11
|
+
t.boolean :enabled
|
12
|
+
t.boolean :autorefresh
|
13
|
+
t.boolean :installer_updates
|
14
|
+
|
15
|
+
t.timestamps null: false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateSccExtendings < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :scc_extendings do |t|
|
4
|
+
t.references :scc_product, index: true, foreign_key: false, null: false
|
5
|
+
t.references :scc_extension, index: true, foreign_key: false, null: false
|
6
|
+
|
7
|
+
t.timestamps null: false
|
8
|
+
end
|
9
|
+
add_foreign_key :scc_extendings, :scc_products, column: :scc_product_id
|
10
|
+
add_foreign_key :scc_extendings, :scc_products, column: :scc_extension_id
|
11
|
+
end
|
12
|
+
end
|