staypuft 0.1.5 → 0.1.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/app/assets/javascripts/staypuft/staypuft.js +99 -1
- data/app/assets/stylesheets/staypuft/bootstrap_and_overrides.css.scss +48 -4
- data/app/assets/stylesheets/staypuft/staypuft.css.scss +7 -11
- data/app/controllers/staypuft/deployments_controller.rb +23 -20
- data/app/controllers/staypuft/steps_controller.rb +18 -24
- data/app/helpers/staypuft/application_helper.rb +16 -1
- data/app/helpers/staypuft/deployments_helper.rb +1 -1
- data/app/lib/actions/staypuft/host/wait_until_host_ready.rb +2 -8
- data/app/lib/staypuft/seeder.rb +702 -0
- data/app/models/staypuft/concerns/host_open_stack_affiliation.rb +29 -2
- data/app/models/staypuft/concerns/hostgroup_extensions.rb +28 -3
- data/app/models/staypuft/concerns/lookup_key_extensions.rb +72 -0
- data/app/models/staypuft/deployment/abstract_param_scope.rb +31 -0
- data/app/models/staypuft/deployment/attribute_param_storage.rb +41 -0
- data/app/models/staypuft/deployment/cinder_service.rb +87 -0
- data/app/models/staypuft/deployment/glance_service.rb +85 -0
- data/app/models/staypuft/deployment/ips.rb +26 -0
- data/app/models/staypuft/deployment/neutron_service.rb +165 -0
- data/app/models/staypuft/deployment/nova_service.rb +84 -0
- data/app/models/staypuft/deployment/passwords.rb +66 -0
- data/app/models/staypuft/deployment/vips.rb +36 -0
- data/app/models/staypuft/deployment.rb +179 -72
- data/app/models/staypuft/deployment_role_hostgroup.rb +1 -1
- data/app/models/staypuft/role.rb +8 -1
- data/app/models/staypuft/service/ui_params.rb +12 -1
- data/app/views/staypuft/deployments/_assigned_hosts_table.html.erb +60 -0
- data/app/views/staypuft/deployments/_deployed_hosts_table.html.erb +51 -0
- data/app/views/staypuft/deployments/_free_hosts_table.html.erb +47 -0
- data/app/views/staypuft/deployments/edit.html.erb +50 -0
- data/app/views/staypuft/deployments/show.html.erb +41 -79
- data/app/views/staypuft/deployments/summary.html.erb +4 -1
- data/app/views/staypuft/steps/_cinder.html.erb +17 -0
- data/app/views/staypuft/steps/_glance.html.erb +16 -0
- data/app/views/staypuft/steps/_neutron.html.erb +57 -0
- data/app/views/staypuft/steps/_nova.html.erb +34 -0
- data/app/views/staypuft/steps/deployment_settings.html.erb +41 -17
- data/app/views/staypuft/steps/services_configuration.html.erb +19 -32
- data/app/views/staypuft/steps/{services_selection.html.erb → services_overview.html.erb} +7 -3
- data/config/routes.rb +2 -0
- data/db/migrate/20140623142500_remove_amqp_provider_from_staypuft_deployment.rb +6 -0
- data/db/seeds.rb +1 -314
- data/lib/staypuft/engine.rb +1 -0
- data/lib/staypuft/version.rb +1 -1
- metadata +23 -3
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
ZjllNGYyYjkzYTQwOTFhMDBiYmVmMzdiN2E4ZjlhY2EyZDRmN2VjNw==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
MzNiNDViNTcwZWJmNTg3N2VlNzAxZDM2MWIzNTVlNDEyZWUyZmQ5Nw==
|
7
7
|
SHA512:
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
NzUzNDAwZTNlMzQxYzdkNDY5YzM2MWNiNTRjMzgyMjNlNDNjMDhlOTQ2OGIz
|
10
|
+
MmE3NTQxMjgyNGNhZTA1ODM4YjJlODY2YzdlNzRlYjMzOThmNmJlZjI2Mjkx
|
11
|
+
YmM1NDE3MTcwNWIzMDc5OWQxODgwYTFhZjBkNjkzOWUzNGQ4OTE=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
MjYyMTg1ODk3NTc2MmQzZmI4YjEzZTRlYzU1NTFiYjhkZmY2YmJmZjk1M2E1
|
14
|
+
MGE3MDU5Y2M3MDQ5Y2RhNDE2NjljYzU1ZWU0MzI4NjViNTFlOTczMzc2ZjM0
|
15
|
+
MGFjZGQyZTI4NGUyMGRmMmNlMmY0ZDNjYzEzZTVlYWM3MzQzNzI=
|
@@ -24,7 +24,105 @@ $(function () {
|
|
24
24
|
var tr = $(this).closest("tr");
|
25
25
|
tr.toggleClass("info", this.checked);
|
26
26
|
if (tr.hasClass("deployed")) {
|
27
|
-
tr.toggleClass("danger",
|
27
|
+
tr.toggleClass("danger", this.checked);
|
28
28
|
}
|
29
29
|
});
|
30
|
+
|
31
|
+
// Workaround to properly activate and deactivate tabs in tabbed_side_nav_table
|
32
|
+
// Should be probably done properly by extending Bootstrap Tab
|
33
|
+
$(".tabbed_side_nav_table").on('click.bs.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
|
34
|
+
e.preventDefault();
|
35
|
+
$(this).closest('ul').find('.activated').removeClass('activated');
|
36
|
+
$(this).addClass("activated");
|
37
|
+
$(this).closest('li').addClass('activated');
|
38
|
+
});
|
39
|
+
|
40
|
+
$('.tabbed_side_nav_table').on('click', 'button.close', function() {
|
41
|
+
$(this).closest('.tab-pane.active').removeClass('active');
|
42
|
+
$(this).closest('.tabbed_side_nav_table').find('.activated').removeClass('activated');
|
43
|
+
})
|
44
|
+
|
45
|
+
var duration = 150;
|
46
|
+
|
47
|
+
showPasswords();
|
48
|
+
$("input[name='staypuft_deployment[passwords][mode]']").change(showPasswords);
|
49
|
+
function showPasswords() {
|
50
|
+
if ($('#staypuft_deployment_passwords_mode_single').is(":checked")) {
|
51
|
+
$('.single_password').fadeIn(duration);
|
52
|
+
}
|
53
|
+
else {
|
54
|
+
$('.single_password').fadeOut(duration)
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
showNovaVlanRange();
|
59
|
+
$("input[name='staypuft_deployment[nova][network_manager]']").change(showNovaVlanRange);
|
60
|
+
function showNovaVlanRange() {
|
61
|
+
if ($('#staypuft_deployment_nova_network_manager_vlanmanager').is(":checked")) {
|
62
|
+
$('.nova_vlan_range').fadeIn(duration);
|
63
|
+
}
|
64
|
+
else {
|
65
|
+
$('.nova_vlan_range').fadeOut(duration)
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
showNeutronVlanRange();
|
70
|
+
$("input[name='staypuft_deployment[neutron][network_segmentation]']").change(showNeutronVlanRange);
|
71
|
+
function showNeutronVlanRange() {
|
72
|
+
if ($('#staypuft_deployment_neutron_network_segmentation_vlan').is(":checked")) {
|
73
|
+
$('.neutron_tenant_vlan_ranges').fadeIn(duration);
|
74
|
+
}
|
75
|
+
else {
|
76
|
+
$('.neutron_tenant_vlan_ranges').fadeOut(duration)
|
77
|
+
}
|
78
|
+
}
|
79
|
+
|
80
|
+
showNeutronExternalInterface();
|
81
|
+
$("input[name='staypuft_deployment[neutron][use_external_interface]']").change(showNeutronExternalInterface);
|
82
|
+
function showNeutronExternalInterface() {
|
83
|
+
if ($('#staypuft_deployment_neutron_use_external_interface').is(":checked")) {
|
84
|
+
$('.neutron_external_interface').fadeIn(duration);
|
85
|
+
}
|
86
|
+
else {
|
87
|
+
$('.neutron_external_interface').fadeOut(duration)
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
showNeutronExternalVlan();
|
92
|
+
$("input[name='staypuft_deployment[neutron][use_vlan_for_external_network]']").change(showNeutronExternalVlan);
|
93
|
+
function showNeutronExternalVlan() {
|
94
|
+
if ($('#staypuft_deployment_neutron_use_vlan_for_external_network').is(":checked")) {
|
95
|
+
$('.neutron_external_vlan').fadeIn(duration);
|
96
|
+
}
|
97
|
+
else {
|
98
|
+
$('.neutron_external_vlan').fadeOut(duration)
|
99
|
+
}
|
100
|
+
}
|
101
|
+
|
102
|
+
showGlanceNfsNetworkPath();
|
103
|
+
$("input[name='staypuft_deployment[glance][driver_backend]']").change(showGlanceNfsNetworkPath);
|
104
|
+
function showGlanceNfsNetworkPath() {
|
105
|
+
if ($('#staypuft_deployment_glance_driver_backend_nfs').is(":checked")) {
|
106
|
+
$('.glance_nfs_network_path').show();
|
107
|
+
}
|
108
|
+
else {
|
109
|
+
$('.glance_nfs_network_path').hide();
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
showCinderNfsUri();
|
114
|
+
$("input[name='staypuft_deployment[cinder][driver_backend]']").change(showCinderNfsUri);
|
115
|
+
function showCinderNfsUri() {
|
116
|
+
if ($('#staypuft_deployment_cinder_driver_backend_nfs').is(":checked")) {
|
117
|
+
$('.cinder_nfs_uri').show();
|
118
|
+
}
|
119
|
+
else {
|
120
|
+
$('.cinder_nfs_uri').hide();
|
121
|
+
}
|
122
|
+
}
|
123
|
+
|
124
|
+
if ($('.configuration').length > 0) {
|
125
|
+
$('.configuration').find('li').first().find('a')[0].click();
|
126
|
+
}
|
127
|
+
|
30
128
|
});
|
@@ -86,12 +86,12 @@
|
|
86
86
|
}
|
87
87
|
|
88
88
|
.tabbed_side_nav_table{
|
89
|
-
.nav
|
89
|
+
.nav {
|
90
90
|
padding-top: 5px;
|
91
91
|
padding-right: 4px;
|
92
|
-
& > li.
|
92
|
+
& > li.activated {
|
93
93
|
z-index: 1;
|
94
|
-
|
94
|
+
&:after, &:before {
|
95
95
|
content: "";
|
96
96
|
position: absolute;
|
97
97
|
top: 0px;
|
@@ -103,13 +103,51 @@
|
|
103
103
|
border-color: rgba(238, 238, 238, 0) rgb(238, 238, 238) rgba(238, 238, 238, 0) rgba(238, 238, 238, 0);
|
104
104
|
-webkit-transform:rotate(360deg);
|
105
105
|
}
|
106
|
-
|
106
|
+
&:before {
|
107
107
|
border-width: 21px 16px 21px 0;
|
108
108
|
top: -1px;
|
109
109
|
right: -9px;
|
110
110
|
border-right-color: rgb(217, 217, 217);
|
111
111
|
}
|
112
112
|
}
|
113
|
+
li {
|
114
|
+
border: 1px solid rgb(227, 227, 227);
|
115
|
+
height: 42px;
|
116
|
+
margin-bottom: 3px;
|
117
|
+
border-radius: 4px;
|
118
|
+
line-height: 40px;
|
119
|
+
box-sizing: border-box;
|
120
|
+
padding: 0;
|
121
|
+
text-align: center;
|
122
|
+
&:hover, &.activated {
|
123
|
+
background: rgb(238, 238, 238);
|
124
|
+
}
|
125
|
+
& > div {
|
126
|
+
padding: 0;
|
127
|
+
div.group_name {
|
128
|
+
text-align: left;
|
129
|
+
padding: 0 8px;
|
130
|
+
}
|
131
|
+
&:not(:first-child) {
|
132
|
+
border-left: 1px solid rgb(227, 227, 227);
|
133
|
+
}
|
134
|
+
&:last-child > a {
|
135
|
+
border-top-right-radius: 4px;
|
136
|
+
border-bottom-right-radius: 4px;
|
137
|
+
}
|
138
|
+
& > a {
|
139
|
+
box-sizing: border-box;
|
140
|
+
display: block;
|
141
|
+
&.activated {
|
142
|
+
background: $brand-primary;
|
143
|
+
color: white;
|
144
|
+
}
|
145
|
+
&.disabled {
|
146
|
+
color: $btn-link-disabled-color;
|
147
|
+
}
|
148
|
+
}
|
149
|
+
}
|
150
|
+
}
|
113
151
|
}
|
114
152
|
.tab-content {
|
115
153
|
padding-left: 4px;
|
@@ -119,3 +157,9 @@
|
|
119
157
|
}
|
120
158
|
}
|
121
159
|
}
|
160
|
+
|
161
|
+
.panel-body {
|
162
|
+
ul {
|
163
|
+
padding-left: 1em;
|
164
|
+
}
|
165
|
+
}
|
@@ -24,17 +24,13 @@
|
|
24
24
|
overflow: auto;
|
25
25
|
}
|
26
26
|
|
27
|
-
.nav > li > a.roles_list {
|
28
|
-
padding: 0;
|
29
|
-
border: 1px solid #ddd;
|
30
|
-
& > div {
|
31
|
-
padding: 10px;
|
32
|
-
&:not(:first-child) {
|
33
|
-
border-left: 1px solid #ddd;
|
34
|
-
}
|
35
|
-
}
|
36
|
-
}
|
37
|
-
|
38
27
|
.association.well {
|
39
28
|
min-height: 500px;
|
29
|
+
margin-top: 50px;
|
30
|
+
h4{
|
31
|
+
margin: 6px 0 6px 0;
|
32
|
+
&.pull-left {
|
33
|
+
margin-left: 10px;
|
34
|
+
}
|
35
|
+
}
|
40
36
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
module Staypuft
|
2
|
-
class DeploymentsController < ApplicationController
|
2
|
+
class DeploymentsController < Staypuft::ApplicationController
|
3
3
|
include Foreman::Controller::AutoCompleteSearch
|
4
4
|
|
5
5
|
def index
|
@@ -7,16 +7,7 @@ module Staypuft
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def new
|
10
|
-
|
11
|
-
|
12
|
-
deployment = Deployment.new(:name => Deployment::NEW_NAME_PREFIX+SecureRandom.hex,
|
13
|
-
:amqp_provider => Deployment::AMQP_RABBITMQ)
|
14
|
-
deployment.layout = Layout.where(:name => "Distributed",
|
15
|
-
:networking => "neutron").first
|
16
|
-
deployment_hostgroup = ::Hostgroup.new name: deployment.name, parent: base_hostgroup
|
17
|
-
deployment_hostgroup.save!
|
18
|
-
|
19
|
-
deployment.hostgroup = deployment_hostgroup
|
10
|
+
deployment = Deployment.new(:name => Deployment::NEW_NAME_PREFIX+SecureRandom.hex)
|
20
11
|
deployment.save!
|
21
12
|
|
22
13
|
redirect_to deployment_steps_path(deployment_id: deployment)
|
@@ -33,6 +24,12 @@ module Staypuft
|
|
33
24
|
@service_hostgroup_map = @deployment.services_hostgroup_map
|
34
25
|
end
|
35
26
|
|
27
|
+
# FIXME: missing update action, there is no way how to submit the edited params
|
28
|
+
def edit
|
29
|
+
@deployment = Deployment.find(params[:id])
|
30
|
+
@service_hostgroup_map = @deployment.services_hostgroup_map
|
31
|
+
end
|
32
|
+
|
36
33
|
def destroy
|
37
34
|
Deployment.find(params[:id]).destroy
|
38
35
|
process_success
|
@@ -58,10 +55,7 @@ module Staypuft
|
|
58
55
|
hostgroup = ::Hostgroup.find params[:hostgroup_id]
|
59
56
|
deployment_in_progress = ForemanTasks::Lock.locked?(deployment, nil)
|
60
57
|
|
61
|
-
|
62
|
-
assigned_hosts = hostgroup.hosts
|
63
|
-
hosts_to_assign = targeted_hosts - assigned_hosts
|
64
|
-
hosts_to_remove = assigned_hosts - targeted_hosts
|
58
|
+
hosts_to_assign = ::Host::Base.find Array(params[:host_ids])
|
65
59
|
|
66
60
|
unassigned_hosts = hosts_to_assign.reduce([]) do |unassigned_hosts, discovered_host|
|
67
61
|
success, host = assign_host_to_hostgroup discovered_host, hostgroup
|
@@ -77,11 +71,20 @@ module Staypuft
|
|
77
71
|
join("\n"))
|
78
72
|
end
|
79
73
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
74
|
+
redirect_to show_with_hostgroup_selected_deployment_path(
|
75
|
+
id: deployment, hostgroup_id: hostgroup)
|
76
|
+
end
|
77
|
+
|
78
|
+
def unassign_host
|
79
|
+
deployment = Deployment.find(params[:id])
|
80
|
+
hostgroup = ::Hostgroup.find params[:hostgroup_id]
|
81
|
+
deployment_in_progress = ForemanTasks::Lock.locked?(deployment, nil)
|
82
|
+
|
83
|
+
hosts_to_unassign = ::Host::Base.find Array(params[:host_ids])
|
84
|
+
|
85
|
+
hosts_to_unassign.each do |host|
|
86
|
+
unless host.open_stack_deployed? && deployment_in_progress
|
87
|
+
host.open_stack_unassign
|
85
88
|
host.environment = Environment.get_discovery
|
86
89
|
host.save!
|
87
90
|
host.setBuild
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Staypuft
|
2
|
-
class StepsController < ApplicationController
|
2
|
+
class StepsController < Staypuft::ApplicationController
|
3
3
|
include Wicked::Wizard
|
4
|
-
steps :deployment_settings, :
|
4
|
+
steps :deployment_settings, :services_overview, :services_configuration
|
5
5
|
|
6
6
|
before_filter :get_deployment
|
7
7
|
|
@@ -10,7 +10,7 @@ module Staypuft
|
|
10
10
|
when :deployment_settings
|
11
11
|
@layouts = ordered_layouts
|
12
12
|
when :services_configuration
|
13
|
-
@
|
13
|
+
@services_map = [:nova, :neutron, :glance, :cinder]
|
14
14
|
end
|
15
15
|
|
16
16
|
render_wizard
|
@@ -18,33 +18,27 @@ module Staypuft
|
|
18
18
|
|
19
19
|
def update
|
20
20
|
case step
|
21
|
+
|
21
22
|
when :deployment_settings
|
22
|
-
@layouts
|
23
|
+
@layouts = ordered_layouts
|
24
|
+
# FIXME: validate that deployment is valid when leaving wizard with cancel button
|
25
|
+
@deployment.form_step = Deployment::STEP_SETTINGS
|
26
|
+
@deployment.passwords.attributes = params[:staypuft_deployment].delete(:passwords)
|
27
|
+
@deployment.attributes = params[:staypuft_deployment]
|
28
|
+
|
29
|
+
when :services_overview
|
30
|
+
@deployment.form_step = Deployment::STEP_OVERVIEW
|
23
31
|
|
24
|
-
Deployment.transaction do
|
25
|
-
@deployment.form_step = Deployment::STEP_SETTINGS unless @deployment.form_complete?
|
26
|
-
@deployment.update_attributes(params[:staypuft_deployment])
|
27
|
-
@deployment.update_hostgroup_list
|
28
|
-
@deployment.set_custom_params
|
29
|
-
end
|
30
|
-
when :services_selection
|
31
|
-
@deployment.form_step = Deployment::STEP_SELECTION unless @deployment.form_complete?
|
32
32
|
when :services_configuration
|
33
|
-
|
34
|
-
@service_hostgroup_map = @deployment.services_hostgroup_map
|
33
|
+
@services_map = [:nova, :neutron, :glance, :cinder]
|
35
34
|
if params[:staypuft_deployment]
|
36
|
-
@deployment.form_step = Deployment::STEP_CONFIGURATION
|
37
|
-
|
38
|
-
|
39
|
-
hostgroup = Hostgroup.find(hostgroup_id)
|
40
|
-
hostgroup_params[:puppetclass_params].each do |puppetclass_id, puppetclass_params|
|
41
|
-
puppetclass = Puppetclass.find(puppetclass_id)
|
42
|
-
puppetclass_params.each do |param_name, param_value|
|
43
|
-
hostgroup.set_param_value_if_changed(puppetclass, param_name, param_value)
|
44
|
-
end
|
45
|
-
end
|
35
|
+
@deployment.form_step = Deployment::STEP_CONFIGURATION
|
36
|
+
@services_map.each do |service|
|
37
|
+
@deployment.send(service).attributes = params[:staypuft_deployment].delete(service)
|
46
38
|
end
|
47
39
|
end
|
40
|
+
else
|
41
|
+
raise 'unknown step'
|
48
42
|
end
|
49
43
|
|
50
44
|
render_wizard @deployment
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module Staypuft
|
2
2
|
module ApplicationHelper
|
3
3
|
def radio_button_f_non_inline(f, attr, options = {})
|
4
|
-
text
|
4
|
+
text = options.delete(:text)
|
5
5
|
value = options.delete(:value)
|
6
6
|
content_tag(:div, :class => 'radio') do
|
7
7
|
label_tag('') do
|
@@ -9,5 +9,20 @@ module Staypuft
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
end
|
12
|
+
|
13
|
+
def check_box_f_non_inline(f, attr, options = {})
|
14
|
+
text = options.delete(:text)
|
15
|
+
checked_value = options.delete(:checked_value)
|
16
|
+
unchecked_value = options.delete(:unchecked_value)
|
17
|
+
content_tag(:div, :class => 'checkbox') do
|
18
|
+
label_tag('') do
|
19
|
+
f.check_box(attr, options, checked_value, unchecked_value) + " #{text} "
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def change_label_width(width, html)
|
25
|
+
html.gsub(/class="col-md-2 control-label"/, "class=\"col-md-#{width} control-label\"").html_safe
|
26
|
+
end
|
12
27
|
end
|
13
28
|
end
|
@@ -57,15 +57,10 @@ module Actions
|
|
57
57
|
5
|
58
58
|
end
|
59
59
|
|
60
|
-
# TODO The puppet modules sometimes fail then become ready
|
61
|
-
# after subsequent hosts have started. For this reason
|
62
|
-
# we can not check to see if the host is ready using the
|
63
|
-
# stats on the host only. This needs fixing in the puppet
|
64
|
-
# modules then reflecting here.
|
65
60
|
def host_ready?(host_id)
|
66
61
|
host = ::Host.find(host_id)
|
67
62
|
host.reports.order('reported_at DESC').any? do |report|
|
68
|
-
|
63
|
+
check_for_failures(report, host.id)
|
69
64
|
report_change?(report)
|
70
65
|
end
|
71
66
|
end
|
@@ -74,10 +69,9 @@ module Actions
|
|
74
69
|
report.status['applied'] > 0
|
75
70
|
end
|
76
71
|
|
77
|
-
# TODO To aid logging add the report ID to the exception or
|
78
|
-
# the output object.
|
79
72
|
def check_for_failures(report, id)
|
80
73
|
if report.status['failed'] > 0
|
74
|
+
output[:report_id] = report.id
|
81
75
|
fail(::Staypuft::Exception, "Latest Puppet Run Contains Failures for Host: #{id}")
|
82
76
|
end
|
83
77
|
end
|