foreman_remote_execution 3.2.2 → 3.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +5 -30
  3. data/.github/workflows/ci.yml +101 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop_todo.yml +3 -0
  6. data/Gemfile +1 -0
  7. data/app/assets/stylesheets/foreman_remote_execution/job_invocations.scss +6 -5
  8. data/app/controllers/api/v2/job_invocations_controller.rb +17 -0
  9. data/app/helpers/remote_execution_helper.rb +38 -33
  10. data/app/lib/actions/remote_execution/run_host_job.rb +3 -2
  11. data/app/models/remote_execution_provider.rb +5 -0
  12. data/app/services/default_proxy_proxy_selector.rb +20 -0
  13. data/app/views/api/v2/job_invocations/main.json.rabl +6 -0
  14. data/app/views/job_invocations/_card_target_hosts.html.erb +1 -1
  15. data/app/views/job_invocations/_tab_hosts.html.erb +3 -23
  16. data/app/views/job_invocations/index.html.erb +2 -1
  17. data/app/views/job_invocations/show.html.erb +0 -6
  18. data/app/views/job_invocations/show.json.erb +4 -0
  19. data/app/views/templates/ssh/package_action.erb +1 -0
  20. data/app/views/templates/ssh/puppet_agent_disable.erb +3 -0
  21. data/app/views/templates/ssh/puppet_agent_enable.erb +3 -0
  22. data/app/views/templates/ssh/puppet_install_modules_from_forge.erb +3 -0
  23. data/app/views/templates/ssh/puppet_run_once.erb +3 -0
  24. data/db/seeds.d/70-job_templates.rb +1 -1
  25. data/foreman_remote_execution.gemspec +4 -5
  26. data/lib/foreman_remote_execution/version.rb +1 -1
  27. data/locale/action_names.rb +0 -1
  28. data/package.json +16 -33
  29. data/webpack/__mocks__/foremanReact/common/I18n.js +1 -0
  30. data/webpack/__mocks__/foremanReact/components/common/ActionButtons/ActionButtons.js +3 -0
  31. data/webpack/__mocks__/foremanReact/constants.js +3 -0
  32. data/webpack/index.js +9 -7
  33. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +52 -0
  34. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +8 -0
  35. data/webpack/react_app/components/TargetingHosts/TargetingHostsConsts.js +1 -0
  36. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +12 -0
  37. data/webpack/react_app/components/TargetingHosts/__tests__/HostItem.test.js +6 -0
  38. data/webpack/react_app/components/TargetingHosts/__tests__/HostStatus.test.js +6 -0
  39. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHosts.test.js +6 -0
  40. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostItem.test.js.snap +31 -0
  41. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/HostStatus.test.js.snap +12 -0
  42. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +81 -0
  43. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +43 -0
  44. data/webpack/react_app/components/TargetingHosts/components/HostItem.js +39 -0
  45. data/webpack/react_app/components/TargetingHosts/components/HostStatus.js +54 -0
  46. data/webpack/react_app/components/TargetingHosts/index.js +37 -0
  47. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.js +10 -0
  48. data/webpack/react_app/components/jobInvocations/AggregateStatus/index.test.js +6 -3
  49. data/webpack/react_app/components/jobInvocations/index.js +19 -7
  50. data/webpack/react_app/redux/actions/jobInvocations/index.js +12 -8
  51. data/webpack/react_app/redux/consts.js +1 -2
  52. data/webpack/react_app/redux/reducers/jobInvocations/index.fixtures.js +8 -40
  53. data/webpack/react_app/redux/reducers/jobInvocations/index.test.js +17 -11
  54. data/webpack/test_setup.js +2 -1
  55. metadata +26 -12
  56. data/.hound.yml +0 -19
  57. data/.travis.yml +0 -6
  58. data/app/views/job_invocations/_host_actions_td.html.erb +0 -3
  59. data/app/views/job_invocations/_host_name_td.html.erb +0 -8
  60. data/app/views/job_invocations/_host_status_td.html.erb +0 -1
  61. data/app/views/job_invocations/show.js.erb +0 -23
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 19c80592b90b1ba20ac213bae2ff36e9608202161e29718a063d3ac52ed249b3
4
- data.tar.gz: daa58fc0cfe1a9b788a49372c1d200579502331ef9e1f1424fa4a81270898a8b
3
+ metadata.gz: af3cd3861fdf9ca798dff28f5da9441110f6442b9cb156604325c0c535dd7d27
4
+ data.tar.gz: 24188a80f5231e8d15826d85322ff410a23bb62cded5a6ab5d30a87063eb8b85
5
5
  SHA512:
6
- metadata.gz: c3b9ee1922a5ece712f8f431eb84de7ab5793d5108fe4959ce690a0acd6edb14f21ab5135cc1b7bcf37d3fbe81af45731fe1900e1803683c131bc3dba13aa931
7
- data.tar.gz: 5db356770b3017319ef2e4f0d1a16598025c9227b540661b2f3e6104f2bd522dbbfb351266eae0bc63d19667a2edc079de0136b50e648e6abddf973bbbe58ead
6
+ metadata.gz: bdb4f63c31a63b4a54268f7fa36174cdf11ba90393159862370b29679d90a46ea5e0b8f86a0d98708166fa89fc0af0223dd4f93990fd0bc6e12c9dac473ab6e2
7
+ data.tar.gz: 4c03fa3ead970d7817e00e884a32857a5a17ae1e41462e40db4057a57f4def05f04d6ad1a16829807a883482d3a684fe344b06e367c72a99cc13c1faacb7b673
data/.eslintrc CHANGED
@@ -1,32 +1,7 @@
1
1
  {
2
- "root": true,
3
- "extends": ["airbnb-base", "./node_modules/@theforeman/vendor-dev/eslint.extends.js"],
4
- "plugins": [
5
- "react"
6
- ],
7
- "env": {
8
- "browser": true,
9
- "es6": true,
10
- "node": true,
11
- "jasmine": true,
12
- "jest": true
13
- },
14
- "parser": "babel-eslint",
15
- "rules": {
16
- "react/jsx-uses-vars": "error",
17
- "react/jsx-uses-react": "error",
18
- "no-unused-vars": [
19
- "error",
20
- {
21
- "vars": "all",
22
- "args": "none"
23
- }
24
- ],
25
- "no-underscore-dangle": "off",
26
- "no-use-before-define": "off",
27
- "import/prefer-default-export": "off",
28
- // Import rules off for now due to HoundCI issue
29
- "import/no-unresolved": "off",
30
- "import/extensions": "off"
31
- }
2
+ "plugins": ["@theforeman/foreman"],
3
+ "extends": [
4
+ "plugin:@theforeman/foreman/core",
5
+ "plugin:@theforeman/foreman/plugins"
6
+ ]
32
7
  }
@@ -0,0 +1,101 @@
1
+ name: CI
2
+ on: [pull_request]
3
+ env:
4
+ RAILS_ENV: test
5
+ DATABASE_URL: postgresql://postgres:@localhost/test
6
+ DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: true
7
+ jobs:
8
+ rubocop:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Setup Ruby
13
+ uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: 2.6
16
+ - name: Setup
17
+ run: |
18
+ gem install bundler
19
+ bundle install --jobs=3 --retry=3
20
+ - name: Run rubocop
21
+ run: bundle exec rubocop
22
+ test_ruby:
23
+ runs-on: ubuntu-latest
24
+ needs: rubocop
25
+ services:
26
+ postgres:
27
+ image: postgres:12.1
28
+ ports: ['5432:5432']
29
+ options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
30
+ strategy:
31
+ fail-fast: false
32
+ matrix:
33
+ foreman-core-branch: [2.1-stable, develop]
34
+ ruby-version: [2.5, 2.6]
35
+ node-version: [12]
36
+ steps:
37
+ - run: sudo apt-get update
38
+ - run: sudo apt-get install build-essential libcurl4-openssl-dev zlib1g-dev libpq-dev
39
+ - uses: actions/checkout@v2
40
+ with:
41
+ repository: theforeman/foreman
42
+ ref: ${{ matrix.foreman-core-branch }}
43
+ - uses: actions/checkout@v2
44
+ with:
45
+ path: foreman_remote_execution
46
+ - name: Setup Ruby
47
+ uses: ruby/setup-ruby@v1
48
+ with:
49
+ ruby-version: ${{ matrix.ruby-version }}
50
+ - name: Setup Node
51
+ uses: actions/setup-node@v1
52
+ with:
53
+ node-version: ${{ matrix.node-version }}
54
+ - uses: actions/cache@v1
55
+ with:
56
+ path: vendor/bundle
57
+ key: ${{ runner.os }}-fgems-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
58
+ restore-keys: |
59
+ ${{ runner.os }}-fgems-${{ matrix.ruby-version }}-
60
+ - name: Setup Bundler
61
+ run: |
62
+ echo "gem 'foreman_remote_execution', path: './foreman_remote_execution'" > bundler.d/foreman_remote_execution.local.rb
63
+ gem install bundler
64
+ bundle config set without journald development console libvirt
65
+ bundle config set path vendor/bundle
66
+ - name: Prepare test env
67
+ run: |
68
+ bundle install --jobs=3 --retry=3
69
+ bundle exec rake db:create
70
+ bundle exec rake db:migrate
71
+ - name: Run plugin tests
72
+ run: |
73
+ bundle exec rake test:foreman_remote_execution
74
+ bundle exec rake test TEST="test/unit/foreman/access_permissions_test.rb"
75
+ test_js:
76
+ runs-on: ubuntu-latest
77
+ needs: rubocop
78
+ strategy:
79
+ fail-fast: false
80
+ matrix:
81
+ ruby-version: [2.6]
82
+ node-version: [10, 12]
83
+ steps:
84
+ - uses: actions/checkout@v2
85
+ - name: Setup Ruby
86
+ uses: ruby/setup-ruby@v1
87
+ with:
88
+ ruby-version: ${{ matrix.ruby-version }}
89
+ - name: Setup Node
90
+ uses: actions/setup-node@v1
91
+ with:
92
+ node-version: ${{ matrix.node-version }}
93
+ - name: Nmp install
94
+ run: |
95
+ npm install
96
+ - name: Run plugin linter
97
+ run: |
98
+ npm run lint
99
+ - name: Run plugin tests
100
+ run: |
101
+ npm run test
data/.gitignore CHANGED
@@ -14,3 +14,4 @@ locale/*/*.po.time_stamp
14
14
  Gemfile.lock
15
15
  node_modules/
16
16
  package-lock.json
17
+ coverage/
@@ -6,6 +6,9 @@
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
+ Minitest/GlobalExpectations:
10
+ Enabled: false
11
+
9
12
  # Offense count: 2
10
13
  # Cop supports --auto-correct.
11
14
  # Configuration parameters: TreatCommentsAsGroupSeparators, Include.
data/Gemfile CHANGED
@@ -2,6 +2,7 @@ source 'http://rubygems.org'
2
2
 
3
3
  gemspec :name => 'foreman_remote_execution'
4
4
 
5
+ gem 'rubocop', '~> 0.80.0'
5
6
  gem 'rubocop-minitest'
6
7
  gem 'rubocop-performance'
7
8
  gem 'rubocop-rails'
@@ -1,9 +1,6 @@
1
- div.infoblock {
2
- margin-bottom: 20px;
3
- line-height: 2;
4
-
1
+ .target-hosts-card {
5
2
  pre {
6
- margin-top: 5px;
3
+ white-space:pre-line;
7
4
  }
8
5
  }
9
6
 
@@ -29,3 +26,7 @@ div.infoblock {
29
26
  }
30
27
  }
31
28
  }
29
+
30
+ .text_warp{
31
+ word-wrap: break-word;
32
+ }
@@ -18,11 +18,17 @@ module Api
18
18
 
19
19
  api :GET, '/job_invocations/:id', N_('Show job invocation')
20
20
  param :id, :identifier, :required => true
21
+ param :host_status, :bool, required: false, desc: N_('Show Job status for the hosts')
21
22
  def show
22
23
  @hosts = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
23
24
  @template_invocations = @job_invocation.template_invocations
24
25
  .where(host: @hosts)
25
26
  .includes(:input_values)
27
+
28
+ if params[:host_status] == 'true'
29
+ template_invocations = @template_invocations.includes(:run_host_job_task).to_a
30
+ @host_statuses = Hash[template_invocations.map { |ti| [ti.host_id, template_invocation_status(ti)] }]
31
+ end
26
32
  end
27
33
 
28
34
  def_param_group :job_invocation do
@@ -208,6 +214,17 @@ module Api
208
214
  def parent_scope
209
215
  resource_class.where(nil)
210
216
  end
217
+
218
+ def template_invocation_status(template_invocation)
219
+ task = template_invocation.try(:run_host_job_task)
220
+ parent_task = @job_invocation.task
221
+
222
+ return(parent_task.result == 'cancelled' ? 'cancelled' : 'N/A') if task.nil?
223
+ return task.state if task.state == 'running' || task.state == 'planned'
224
+ return 'error' if task.result == 'warning'
225
+
226
+ task.result
227
+ end
211
228
  end
212
229
  end
213
230
  end
@@ -14,43 +14,35 @@ module RemoteExecutionHelper
14
14
  end
15
15
 
16
16
  def template_invocation_status(task, parent_task)
17
- if task.nil?
18
- if parent_task.result == 'cancelled'
19
- icon_text('warning-triangle-o', _('cancelled'), :kind => 'pficon')
20
- else
21
- icon_text('question', 'N/A', :kind => 'fa')
22
- end
23
- elsif task.state == 'running'
24
- icon_text('running', _('running'), :kind => 'pficon')
25
- elsif task.state == 'planned'
26
- icon_text('build', _('planned'), :kind => 'pficon')
27
- else
28
- case task.result
29
- when 'warning', 'error'
30
- icon_text('error-circle-o', _('failed'), :kind => 'pficon')
31
- when 'cancelled'
32
- icon_text('warning-triangle-o', _('cancelled'), :kind => 'pficon')
33
- when 'success'
34
- icon_text('ok', _('success'), :kind => 'pficon')
35
- else
36
- task.result
37
- end
38
- end
17
+ return(parent_task.result == 'cancelled' ? _('cancelled') : 'N/A') if task.nil?
18
+ return task.state if task.state == 'running' || task.state == 'planned'
19
+ return _('error') if task.result == 'warning'
20
+
21
+ task.result
39
22
  end
40
23
 
41
24
  def template_invocation_actions(task, host, job_invocation, template_invocation)
25
+ links = []
42
26
  host_task = template_invocation.try(:run_host_job_task)
43
- [
44
- display_link_if_authorized(_('Host detail'), hash_for_host_path(host).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer)),
45
- display_link_if_authorized(_('Rerun on %s') % host.name, hash_for_rerun_job_invocation_path(:id => job_invocation, :host_ids => [ host.id ], :authorizer => job_hosts_authorizer)),
46
- if host_task.present?
47
- display_link_if_authorized(
48
- _('Host task'),
49
- hash_for_foreman_tasks_task_path(host_task)
50
- .merge(:auth_object => host_task, :permission => :view_foreman_tasks)
51
- )
52
- end,
53
- ]
27
+
28
+ if authorized_for(hash_for_host_path(host).merge(auth_object: host, permission: :view_hosts, authorizer: job_hosts_authorizer))
29
+ links << { title: _('Host detail'),
30
+ action: { href: host_path(host), 'data-method': 'get', id: "#{host.name}-actions-detail" } }
31
+ end
32
+
33
+ if authorized_for(hash_for_rerun_job_invocation_path(id: job_invocation, host_ids: [ host.id ], authorizer: job_hosts_authorizer))
34
+ links << { title: (_('Rerun on %s') % host.name),
35
+ action: { href: rerun_job_invocation_path(job_invocation, host_ids: [ host.id ]),
36
+ 'data-method': 'get', id: "#{host.name}-actions-rerun" } }
37
+ end
38
+
39
+ if host_task.present? && authorized_for(hash_for_foreman_tasks_task_path(host_task).merge(auth_object: host_task, permission: :view_foreman_tasks))
40
+ links << { title: _('Host task'),
41
+ action: { href: foreman_tasks_task_path(host_task),
42
+ 'data-method': 'get', id: "#{host.name}-actions-task" } }
43
+ end
44
+
45
+ links
54
46
  end
55
47
 
56
48
  def remote_execution_provider_for(template_invocation)
@@ -237,4 +229,17 @@ module RemoteExecutionHelper
237
229
 
238
230
  task.execution_plan.actions[1].try(:input).try(:[], 'script')
239
231
  end
232
+
233
+ def targeting_hosts(job_invocation, hosts)
234
+ hosts.map do |host|
235
+ template_invocation = job_invocation.template_invocations.find { |template_inv| template_inv.host_id == host.id }
236
+ task = template_invocation.try(:run_host_job_task)
237
+ link_authorized = !task.nil? && authorized_for(hash_for_template_invocation_path(:id => template_invocation).merge(:auth_object => host, :permission => :view_hosts, :authorizer => job_hosts_authorizer))
238
+
239
+ { name: host.name,
240
+ link: link_authorized ? template_invocation_path(:id => template_invocation) : '',
241
+ status: template_invocation_status(task, job_invocation.task),
242
+ actions: template_invocation_actions(task, host, job_invocation, template_invocation) }
243
+ end
244
+ end
240
245
  end
@@ -29,6 +29,9 @@ module Actions
29
29
 
30
30
  raise _('Could not use any template used in the job invocation') if template_invocation.blank?
31
31
 
32
+ provider = template_invocation.template.provider
33
+ proxy_selector = provider.required_proxy_selector_for(template_invocation.template) || proxy_selector
34
+
32
35
  provider_type = template_invocation.template.provider_type.to_s
33
36
  proxy = determine_proxy!(proxy_selector, provider_type, host)
34
37
 
@@ -36,8 +39,6 @@ module Actions
36
39
  script = renderer.render
37
40
  raise _('Failed rendering template: %s') % renderer.error_message unless script
38
41
 
39
- provider = template_invocation.template.provider
40
-
41
42
  additional_options = { :hostname => provider.find_ip_or_hostname(host),
42
43
  :script => script,
43
44
  :execution_timeout_interval => job_invocation.execution_timeout_interval,
@@ -98,5 +98,10 @@ class RemoteExecutionProvider
98
98
  def proxy_action_class
99
99
  ForemanRemoteExecutionCore::Actions::RunScript
100
100
  end
101
+
102
+ # Return a specific proxy selector to use for running a given template
103
+ # Returns either nil to use the default selector or an instance of a (sub)class of ::ForemanTasks::ProxySelector
104
+ def required_proxy_selector_for(_template)
105
+ end
101
106
  end
102
107
  end
@@ -0,0 +1,20 @@
1
+ class DefaultProxyProxySelector < ::RemoteExecutionProxySelector
2
+ def initialize
3
+ # TODO: Remove this once we have a reliable way of determining the internal proxy without katello
4
+ # Tracked as https://projects.theforeman.org/issues/29840
5
+ raise _('Internal proxy selector can only be used if Katello is enabled') unless defined?(::Katello)
6
+
7
+ super
8
+ end
9
+
10
+ def available_proxies(host, provider)
11
+ # TODO: Once we have a internal proxy marker/feature on the proxy, we can
12
+ # swap the implementation
13
+ raise _('default_capsule method missing from SmartProxy') unless ::SmartProxy.respond_to?(:default_capsule)
14
+
15
+ internal_proxy = ::SmartProxy.default_capsule
16
+ super.reduce({}) do |acc, (key, proxies)|
17
+ acc.merge(key => proxies.select { |proxy| proxy == internal_proxy })
18
+ end
19
+ end
20
+ end
@@ -21,6 +21,12 @@ child :targeting do
21
21
 
22
22
  child @hosts do
23
23
  extends 'api/v2/hosts/base'
24
+
25
+ if params[:host_status] == 'true'
26
+ node :job_status do |host|
27
+ @host_statuses[host.id]
28
+ end
29
+ end
24
30
  end
25
31
  end
26
32
 
@@ -1,5 +1,5 @@
1
1
  <% template_invocations = job_invocation.pattern_template_invocations %>
2
- <div class="card-pf card-pf-accented">
2
+ <div class="card-pf card-pf-accented target-hosts-card">
3
3
  <div class="card-pf-title">
4
4
  <h2 style="height: 18px;" class="card-pf-title">
5
5
  <%= _('Target hosts') %>
@@ -15,29 +15,9 @@
15
15
  <% end %>
16
16
  <br>
17
17
 
18
- <table class="<%= table_css_classes('table-condensed') %>">
19
- <thead>
20
- <tr>
21
- <th><%= sort :name, :as => _('Host') %></th>
22
- <th><%= _('Status') %></th>
23
- <th><%= _('Actions') %></th>
24
- </tr>
25
- </thead>
26
-
27
- <tbody>
28
- <% hosts.each do |host| %>
29
- <% template_invocation = job_invocation.template_invocations.find { |template_invocation| template_invocation.host_id == host.id } %>
30
- <% task = template_invocation.try(:run_host_job_task) %>
31
- <tr>
32
- <% options = { :host => host, :task => task, :job_invocation => job_invocation, :template_invocation => template_invocation } %>
33
- <%= render 'host_name_td', options %>
34
- <%= render 'host_status_td', options %>
35
- <%= render 'host_actions_td', options %>
36
- </tr>
37
- <% end %>
38
- </tbody>
39
- </table>
40
-
18
+ <div id="targeting_hosts">
19
+ <%= mount_react_component('TargetingHosts', '#targeting_hosts') %>
20
+ </div>
41
21
  <%= will_paginate_with_info @hosts, :container => true %>
42
22
  <% else %>
43
23
  <div class="alert alert-warning">
@@ -1,3 +1,4 @@
1
+ <% stylesheet 'foreman_remote_execution/foreman_remote_execution' %>
1
2
  <% title _('Job invocations') %>
2
3
 
3
4
  <% title_actions(job_invocations_buttons) %>
@@ -19,7 +20,7 @@
19
20
  <tbody>
20
21
  <% @job_invocations.each do |invocation| %>
21
22
  <tr>
22
- <td><%= link_to_if_authorized invocation_description(invocation), hash_for_job_invocation_path(invocation).merge(:auth_object => invocation, :permission => :view_job_invocations, :authorizer => authorizer) %></td>
23
+ <td class="text_warp"><%= link_to_if_authorized invocation_description(invocation), hash_for_job_invocation_path(invocation).merge(:auth_object => invocation, :permission => :view_job_invocations, :authorizer => authorizer) %></td>
23
24
  <td><%= trunc_with_tooltip(invocation&.targeting&.search_query, 15) %></td>
24
25
  <td><%= link_to_invocation_task_if_authorized(invocation) %></td>
25
26
  <td><%= invocation_result(invocation, :success_count) %></td>
@@ -41,9 +41,3 @@
41
41
  <% end %>
42
42
  <%= render_tab_content_for(:main_tabs, subject: @job_invocation) %>
43
43
  </div>
44
-
45
- <script id="job_invocation_refresh" data-refresh-url="<%= job_invocation_path(@job_invocation) %>">
46
- <% if @auto_refresh %>
47
- delayed_refresh($('script#job_invocation_refresh').data('refresh-url'), job_invocation_refresh_data());
48
- <% end %>
49
- </script>