ironfan 4.12.3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,218 @@
1
1
  module Ironfan
2
2
  class Dsl
3
3
 
4
+ class MachineManifest
5
+ include Gorillib::Model
6
+
7
+ # base server fields
8
+ field :environment, Symbol
9
+ field :name, String
10
+ field :cluster_name, String
11
+ field :facet_name, String
12
+ field :components, Array, of: Ironfan::Dsl::Component, default: []
13
+ field :run_list, Array, of: String, default: []
14
+ field :cluster_default_attributes, Hash
15
+ field :cluster_override_attributes, Hash
16
+ field :facet_default_attributes, Hash
17
+ field :facet_override_attributes, Hash
18
+
19
+ # cloud fields
20
+ field :cloud_name, String
21
+ field :availability_zones, Array, default: []
22
+ field :backing, String
23
+ field :ebs_optimized, :boolean
24
+ field :flavor, String
25
+ field :image_id, String
26
+ field :placement_group, String
27
+ field :elastic_ip, String
28
+ field :auto_elastic_ip, String
29
+ field :allocation_id, String
30
+ field :region, String
31
+ field :ssh_user, String
32
+ field :subnet, String
33
+ field :vpc, String
34
+
35
+ #-----------------------------------------------------------------------------------
36
+ # # FIXME: I haven't determined how to pull some of these fields
37
+ # # in from the remote machines. In fact, some of these
38
+ # # will have to be omitted when comparing. Since
39
+ # # they'll only be necessary when we refactor the
40
+ # # backend to accept manifests for launch, I'm going
41
+ # # to leave these commented out for now. --josh
42
+ #
43
+ # # base server fields
44
+ #
45
+ # field :volumes, Array, of: Volume
46
+ #
47
+ # # cloud fields
48
+ #
49
+ # field :bits, Integer
50
+ # field :bootstrap_distro, String
51
+ # field :chef_client_script, String
52
+ # field :default_availability_zone, String
53
+ # field :elastic_load_balancers, Array, of: Ironfan::Dsl::Ec2::ElasticLoadBalancer, default: []
54
+ # field :iam_server_certificates, Array, of: Ironfan::Dsl::Ec2::IamServerCertificate, default: []
55
+ # field :image_name, String
56
+ # field :keypair, String
57
+ # field :monitoring, String
58
+ # field :mount_ephemerals, Hash
59
+ # field :permanent, :boolean
60
+ # field :provider, Whatever
61
+ # field :security_groups, Array, of: Ironfan::Dsl::Ec2::SecurityGroup
62
+ # field :ssh_identity_dir, String
63
+ # field :validation_key, String
64
+ #-----------------------------------------------------------------------------------
65
+
66
+ # Reconstruct machine manifest from a computer, pulling
67
+ # information from remote sources as necessary.
68
+ def self.from_computer(computer)
69
+ node = get_node(computer.name)
70
+ cluster_name = node['cluster_name']
71
+ facet_name = node['facet_name']
72
+ instance = node['facet_index']
73
+
74
+ from_remote(
75
+ cluster_name,
76
+ facet_name,
77
+ instance,
78
+ node,
79
+ computer.machine,
80
+ computer.server.clouds.to_a.first,
81
+ get_role("#{cluster_name}-cluster"),
82
+ get_role("#{cluster_name}-#{facet_name}-facet")
83
+ )
84
+ end
85
+
86
+ def self.from_remote(cluster_name,
87
+ facet_name,
88
+ instance,
89
+ node,
90
+ machine,
91
+ cloud,
92
+ cluster_role,
93
+ facet_role)
94
+ machine = NilCheckDelegate.new(machine)
95
+ cloud = NilCheckDelegate.new(cloud)
96
+ cluster_role = NilCheckDelegate.new(cluster_role)
97
+ facet_role = NilCheckDelegate.new(facet_role)
98
+
99
+ result = Ironfan::Dsl::MachineManifest.
100
+ receive(
101
+
102
+ # base server fields
103
+
104
+ environment: node.chef_environment,
105
+ name: instance,
106
+ cluster_name: cluster_name,
107
+ facet_name: facet_name,
108
+ components: remote_components(node),
109
+ run_list: remote_run_list(node),
110
+ cluster_default_attributes: (cluster_role.default_attributes || {}),
111
+ cluster_override_attributes: (cluster_role.override_attributes || {}),
112
+ facet_default_attributes: (facet_role.default_attributes || {}),
113
+ facet_override_attributes: (facet_role.override_attributes || {}),
114
+
115
+ # cloud fields
116
+
117
+ backing: machine.root_device_type,
118
+ cloud_name: cloud.name,
119
+ availability_zones: [*machine.availability_zone],
120
+ ebs_optimized: machine.ebs_optimized,
121
+ flavor: machine.flavor_id,
122
+ image_id: machine.image_id,
123
+ keypair: machine.nilcheck_depth(1).key_pair.name,
124
+ monitoring: machine.monitoring,
125
+ placement_group: machine.placement_group,
126
+ region: machine.availability_zone.to_s[/.*-.*-\d+/],
127
+ security_groups: machine.nilcheck_depth(1).groups.map{|x| {name: x}},
128
+ subnet: machine.subnet_id,
129
+ vpc: machine.vpc_id
130
+
131
+ #-----------------------------------------------------------------------------------
132
+ # # FIXME: I haven't determined how to pull some of these fields
133
+ # # in from the remote machines. In fact, some of these
134
+ # # will have to be omitted when comparing. Since
135
+ # # they'll only be necessary when we refactor the
136
+ # # backend to accept manifests for launch, I'm going
137
+ # # to leave these commented out for now. --josh
138
+ #
139
+ # # base server fields
140
+ #
141
+ # volume: local_manifest.volume
142
+ #
143
+ # # cloud fields
144
+ #
145
+ # bits: local_manifest.bits,
146
+ # bootstrap_distro: local_manifest.bootstrap_distro,
147
+ # chef_client_script: local_manifest.chef_client_script,
148
+ # default_availability_zone: local_manifest.default_availability_zone,
149
+ # iam_server_certificates: launch_description.fetch(:iam_server_certificates),
150
+ # image_name: local_manifest.image_name,
151
+ # elastic_load_balancers: launch_description.fetch(:elastic_load_balancers),
152
+ # mount_ephemerals: local_manifest.mount_ephemerals,
153
+ # permanent: local_manifest.permanent,
154
+ # provider: local_manifest.provider,
155
+ # elastic_ip: local_manifest.elastic_ip,
156
+ # auto_elastic_ip: local_manifest.auto_elastic_ip,
157
+ # allocation_id: local_manifest.allocation_id,
158
+ # ssh_user: local_manifest.ssh_user,
159
+ # ssh_identity_dir: local_manifest.ssh_identity_dir,
160
+ # validation_key: local_manifest.validation_key,
161
+ #-----------------------------------------------------------------------------------
162
+ )
163
+ end
164
+
165
+ def to_comparable
166
+ deep_stringify(to_wire.tap do |hsh|
167
+ hsh.delete(:_type)
168
+ hsh.delete(:ssh_user)
169
+ #hsh[:security_groups] = Hash[hsh[:security_groups].map{|x| [x.fetch(:name), x]}]
170
+ hsh[:components] = Hash[hsh.fetch(:components).map do |component|
171
+ [component.fetch(:name), component]
172
+ end]
173
+ hsh[:run_list] = hsh.fetch(:run_list).map do |x|
174
+ x.end_with?(']') ? x : "recipe[#{x}]"
175
+ end
176
+ end)
177
+ end
178
+
179
+ private
180
+
181
+ def deep_stringify obj
182
+ case obj
183
+ when Hash then Hash[obj.map{|k,v| [k.to_s, deep_stringify(v)]}]
184
+ when Array then obj.map{|x| deep_stringify(x)}
185
+ when Symbol then obj.to_s
186
+ else obj
187
+ end
188
+ end
189
+
190
+ def self.get_node(node_name)
191
+ Chef::Node.load(node_name)
192
+ rescue Net::HTTPServerException => ex
193
+ Chef::Node.new
194
+ end
195
+
196
+ def self.get_role(role_name)
197
+ Chef::Role.load(role_name)
198
+ rescue Net::HTTPServerException => ex
199
+ Chef::Role.new
200
+ end
201
+
202
+ def self.remote_components(node)
203
+ announcements = node['components'] || {}
204
+ node['components'].to_a.map do |_, announce|
205
+ name = announce['name'].to_sym
206
+ plugin = Ironfan::Dsl::Compute.plugin_for(name)
207
+ plugin.from_node(node).tap{|x| x.name = name} if plugin
208
+ end.compact
209
+ end
210
+
211
+ def self.remote_run_list(node)
212
+ node.run_list.to_a.map(&:to_s)
213
+ end
214
+ end
215
+
4
216
  class Server < Ironfan::Dsl::Compute
5
217
  field :cluster_name, String
6
218
  field :facet_name, String
@@ -38,6 +250,81 @@ module Ironfan
38
250
  errors['missing cluster/facet/server'] = [cluster_name, facet_name, name] unless (cluster_name && facet_name && name)
39
251
  errors
40
252
  end
253
+
254
+ def to_machine_manifest
255
+ cloud = clouds.each.to_a.first
256
+ MachineManifest.receive(
257
+
258
+ # base server fields
259
+
260
+ environment: environment,
261
+ name: name,
262
+ cluster_name: cluster_name,
263
+ facet_name: facet_name,
264
+ run_list: run_list,
265
+ components: components,
266
+ cluster_default_attributes: cluster_role.default_attributes,
267
+ cluster_override_attributes: cluster_role.override_attributes,
268
+ facet_default_attributes: facet_role.default_attributes,
269
+ facet_override_attributes: facet_role.override_attributes,
270
+ volumes: volumes,
271
+
272
+ # cloud fields
273
+
274
+ cloud_name: cloud.name,
275
+
276
+ availability_zones: cloud.availability_zones,
277
+ backing: cloud.backing,
278
+ bits: cloud.bits,
279
+ bootstrap_distro: cloud.bootstrap_distro,
280
+ chef_client_script: cloud.chef_client_script,
281
+ default_availability_zone: cloud.default_availability_zone,
282
+ elastic_load_balancers: cloud.elastic_load_balancers,
283
+ ebs_optimized: cloud.ebs_optimized,
284
+ flavor: cloud.flavor,
285
+ iam_server_certificates: cloud.iam_server_certificates,
286
+ image_id: cloud.image_id,
287
+ image_name: cloud.image_name,
288
+ keypair: cloud.keypair,
289
+ monitoring: cloud.monitoring,
290
+ mount_ephemerals: cloud.mount_ephemerals,
291
+ permanent: cloud.permanent,
292
+ placement_group: cloud.placement_group,
293
+ provider: cloud.provider,
294
+ elastic_ip: cloud.elastic_ip,
295
+ auto_elastic_ip: cloud.auto_elastic_ip,
296
+ allocation_id: cloud.allocation_id,
297
+ region: cloud.region,
298
+ security_groups: cloud.security_groups,
299
+ ssh_user: cloud.ssh_user,
300
+ ssh_identity_dir: cloud.ssh_identity_dir,
301
+ subnet: cloud.subnet,
302
+ validation_key: cloud.validation_key,
303
+ vpc: cloud.vpc
304
+
305
+ )
306
+ end
307
+
308
+ def canonical_machine_manifest_hash
309
+ self.class.canonicalize(to_machine_manifest)
310
+ end
311
+
312
+ private
313
+
314
+ def self.canonicalize(item)
315
+ case item
316
+ when Array, Gorillib::ModelCollection
317
+ item.each.map{|i| canonicalize(i)}
318
+ when Ironfan::Dsl::Component
319
+ canonicalize(item.to_manifest)
320
+ when Gorillib::Builder, Gorillib::Model
321
+ canonicalize(item.to_wire.tap{|x| x.delete(:_type)})
322
+ when Hash then
323
+ Hash[item.sort.map{|k,v| [k, canonicalize(v)]}]
324
+ else
325
+ item
326
+ end
327
+ end
41
328
  end
42
329
 
43
330
  end
@@ -28,6 +28,7 @@ module Ironfan
28
28
  VOLUME_IDS.merge!({
29
29
  :blank_xfs => 'snap-d9c1edb1',
30
30
  :blank_xfs_tokyo => 'snap-049d1921',
31
+ :blank_xfs_california => 'snap-514b5c5a', # us-west-1
31
32
  })
32
33
 
33
34
  def snapshot_id(*)
data/lib/ironfan/dsl.rb CHANGED
@@ -2,6 +2,138 @@ module Ironfan
2
2
 
3
3
  class Dsl < Builder
4
4
  include Gorillib::Resolution
5
- end
6
5
 
6
+ def self.default_cookbook_reqs
7
+ @default_cookbook_reqs ||= []
8
+ end
9
+
10
+ def self.cookbook_req name, constraint
11
+ default_cookbook_reqs << new_req(name, constraint)
12
+
13
+ end
14
+
15
+ def join_req req1, req2
16
+ # order requirements by operation: =, >=, ~>
17
+ req1, req2 = (req1.constraint < req2.constraint) ? [req1, req2] : [req2, req1]
18
+ cn1, cn2 = [req1.constraint, req2.constraint]
19
+ vers1, vers2 = [cn1.split.last, cn2.split.last]
20
+ vers1_c, vers2_c = [vers1.split('.'), vers2.split('.')]
21
+ op1, op2 = [req1, req2].map{|x| x.constraint.split.first}
22
+
23
+ if op1 == '=' and op2 == '='
24
+ join_eq_eq(req1, req2)
25
+ elsif op1 == '>=' and op2 == '>='
26
+ join_geq_geq(req1, req2)
27
+ elsif op1 == '~>' and op2 == '~>'
28
+ join_agt_agt(req1, req2)
29
+ elsif op1 == '=' and op2 == '>='
30
+ join_eq_gte(req1, req2)
31
+ elsif op1 == '=' and op2 == '~>'
32
+ join_eq_agt(req1, req2)
33
+ elsif op1 == '>=' and op2 == '~>'
34
+ join_gte_agt(req1, req2)
35
+ end
36
+ end
37
+
38
+ def cookbook_req name, constraint
39
+ (@cookbook_reqs ||= []) << self.class.new_req(name, constraint)
40
+ end
41
+
42
+ def children() [] end
43
+
44
+ def cookbook_reqs
45
+ Hash[_cookbook_reqs.map{|x| [x.name, x.constraint]}]
46
+ end
47
+
48
+ def _cookbook_reqs
49
+ [
50
+ *shallow_cookbook_reqs,
51
+ *child_cookbook_reqs
52
+ ].group_by(&:name).values.map do |group|
53
+ group.inject{|result, req| join_req(result, req)}
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ #-----------------------------------------------------------------------------------------------
60
+
61
+ def join_eq_eq(req1, req2)
62
+ (vers(req1) == vers(req2)) ? req1 : bad_reqs(req1, req2)
63
+ end
64
+
65
+ def join_geq_geq(req1, req2)
66
+ (vers(req1) >= vers(req2)) ? req1 : req2
67
+ end
68
+
69
+ def join_agt_agt(req1, req2)
70
+ if vers_a(req1).size == vers_a(req2).size and
71
+ vers_a_head(req1).zip(vers_a_head(req2)).all?{|v1,v2| v1 == v2}
72
+ (req1.constraint > req2.constraint) ? req1 : req2
73
+ else
74
+ bad_reqs(req1, req2)
75
+ end
76
+ end
77
+
78
+ def join_eq_gte(req1, req2)
79
+ vers(req1) >= vers(req2) ? req1 : bad_reqs(req1, req2)
80
+ end
81
+
82
+ def join_eq_agt(req1, req2)
83
+ if match_v_head(req1, req2) and
84
+ vers_a(req1)[vers_a(req2).size - 1] >= vers_a(req2).last
85
+ req1
86
+ else
87
+ bad_reqs(req1, req2)
88
+ end
89
+ end
90
+
91
+ def join_gte_agt(req1, req2)
92
+ if match_v_head(req1, req2) and vers(req1) <= vers(req2)
93
+ req2
94
+ else
95
+ bad_reqs(req1, req2)
96
+ end
97
+ end
98
+
99
+ def match_v_head(req1, req2)
100
+ vers_a_head(req1, vers_a(req2).size).zip(vers_a_head(req2)).all?{|v1,v2| v1 == v2}
101
+ end
102
+
103
+ def op(req)
104
+ req.constraint.split.first
105
+ end
106
+
107
+ def vers(req)
108
+ req.constraint.split.last
109
+ end
110
+
111
+ def vers_a(req)
112
+ vers(req).split('.')
113
+ end
114
+
115
+ def vers_a_head(req, last = 0)
116
+ vers_a(req)[0...last-1]
117
+ end
118
+
119
+ #-----------------------------------------------------------------------------------------------
120
+
121
+ def bad_reqs(req1, req2)
122
+ raise ArgumentError.new("#{req1.name}: cannot reconcile #{req1.constraint} with #{req2.constraint}")
123
+ end
124
+
125
+ def child_cookbook_reqs
126
+ children.map(&:_cookbook_reqs).flatten(1)
127
+ end
128
+
129
+ def self.new_req(name, constraint)
130
+ raise StandardError.new("Please don't use >= constraints. They're too vague!") if
131
+ constraint.start_with?('>=') and not (@@testing ||= false)
132
+ Ironfan::Plugin::CookbookRequirement.new(name: name, constraint: constraint)
133
+ end
134
+
135
+ def shallow_cookbook_reqs
136
+ @cookbook_reqs || self.class.default_cookbook_reqs
137
+ end
138
+ end
7
139
  end
@@ -15,9 +15,11 @@ module Ironfan
15
15
  end
16
16
 
17
17
  class Dsl < Builder
18
+ class Component < Ironfan::Dsl; end
18
19
  class Compute < Ironfan::Dsl; end
19
20
  class Cluster < Ironfan::Dsl::Compute; end
20
21
  class Facet < Ironfan::Dsl::Compute; end
22
+ class Realm < Ironfan::Dsl::Compute; end
21
23
  class Server < Ironfan::Dsl::Compute; end
22
24
 
23
25
  class Role < Ironfan::Dsl; end
@@ -36,6 +38,11 @@ module Ironfan
36
38
  end
37
39
  end
38
40
 
41
+ module Plugin
42
+ class CookbookRequirement; end
43
+ module Base; end
44
+ end
45
+
39
46
  class Provider < Builder
40
47
  class Resource < Builder; end
41
48
  end
@@ -0,0 +1,89 @@
1
+ require 'gorillib/model'
2
+ require 'gorillib/builder'
3
+ require 'gorillib/string/inflections'
4
+ require 'gorillib/metaprogramming/concern'
5
+
6
+ Gorillib::Model::Field.class_eval do
7
+ field :node_attr, String, default: nil
8
+ end
9
+
10
+ module Ironfan
11
+
12
+ module Pluggable
13
+ def add_plugin name, cls
14
+ registry[name] = cls
15
+ end
16
+ def plugin_for name
17
+ registry[name]
18
+ end
19
+ def registry() @registry ||= {}; end
20
+ end
21
+
22
+ module Plugin
23
+ class CookbookRequirement
24
+ include Gorillib::Builder
25
+
26
+ magic :name, String
27
+ magic :constraint, String
28
+
29
+ def <=>(other)
30
+ self.name <=> other.name
31
+ end
32
+ end
33
+
34
+ module Base
35
+ extend Gorillib::Concern
36
+
37
+ def to_node
38
+ Chef::Node.new.tap do |node|
39
+ self.class.fields.select{|_,x| x.node_attr}.each do |_,x|
40
+ val = send(x.name)
41
+ (keys = x.node_attr.split('.'))[0...-1].inject(node.set) do |hsh,key|
42
+ hsh[key]
43
+ end[keys.last] = val unless val.nil?
44
+ end
45
+ end
46
+ end
47
+
48
+ module ClassMethods
49
+ attr_reader :cookbook_reqs
50
+ attr_reader :plugin_name
51
+
52
+ def from_node(node = NilCheckDelegate.new(nil))
53
+ new(Hash[
54
+ fields.select{|_,x| x.node_attr}.map do |_,x|
55
+ [x.name, (x.node_attr.split('.').inject(node) do |hsh,attr|
56
+ if hsh
57
+ (val = hsh[attr]).is_a?(Mash) ? val.to_hash : val
58
+ end
59
+ end)]
60
+ end.reject{|_,v| v.nil?}
61
+ ])
62
+ end
63
+
64
+ def register_with cls, &blk
65
+ (@dest_class = cls).class_eval{ extend Ironfan::Pluggable }
66
+ end
67
+
68
+ def template plugin_name_parts, base_class=self, &blk
69
+ plugin_name_parts = [*plugin_name_parts]
70
+ full_name = plugin_name_parts.map(&:to_s).join('_').to_sym
71
+ plugin_name = plugin_name_parts.first.to_sym
72
+
73
+ Class.new(base_class, &blk).tap do |plugin_class|
74
+ plugin_class.class_eval{ @plugin_name = plugin_name }
75
+
76
+ self.const_set(full_name.to_s.camelize.to_sym, plugin_class)
77
+
78
+ @dest_class.class_eval do
79
+ add_plugin(full_name, plugin_class)
80
+ define_method(full_name) do |*args, &blk|
81
+ plugin_class.plugin_hook self, (args.first || {}), plugin_name, full_name, &blk
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -69,6 +69,8 @@ module Ironfan
69
69
  groups_to_create = [ ]
70
70
  authorizations_to_ensure = [ ]
71
71
 
72
+ computers.each{|comp| ensure_groups(comp) if Ec2.applicable(comp) } # Add facet and cluster security groups for the computer
73
+
72
74
  # First, deduce the list of all groups to which at least one instance belongs
73
75
  # We'll use this later to decide whether to create groups, or authorize access,
74
76
  # using a VPC security group or an EC2 security group.
@@ -76,7 +78,6 @@ module Ironfan
76
78
  groups_to_create << groups_that_should_exist
77
79
 
78
80
  computers.select { |computer| Ec2.applicable computer }.each do |computer|
79
- ensure_groups(computer) # Add facet and cluster security groups for the computer
80
81
  cloud = computer.server.cloud(:ec2)
81
82
  cluster_name = computer.server.cluster_name
82
83
 
@@ -2,13 +2,19 @@
2
2
  require 'gorillib/builder'
3
3
  require 'gorillib/resolution'
4
4
 
5
+ require 'gorillib/nil_check_delegate'
6
+
5
7
  # Pre-declaration of class hierarchy
6
8
  require 'ironfan/headers'
7
9
 
10
+ # Ironfan plugin mixin
11
+ require 'ironfan/plugin/base'
12
+
8
13
  # DSL for cluster descriptions
9
14
  require 'ironfan/dsl'
10
15
  require 'ironfan/builder'
11
16
 
17
+ require 'ironfan/dsl/component'
12
18
  require 'ironfan/dsl/compute'
13
19
  require 'ironfan/dsl/server'
14
20
  require 'ironfan/dsl/facet'
data/lib/ironfan.rb CHANGED
@@ -91,6 +91,18 @@ module Ironfan
91
91
  end
92
92
  end
93
93
 
94
+ def self.load_realm(name)
95
+ name = name.to_sym
96
+ raise ArgumentError, "Please supply a realm name" if name.to_s.empty?
97
+ return @@realms[name] if @@realms[name]
98
+
99
+ load_cluster_files
100
+
101
+ unless @@realms[name] then die("Couldn't find a realm definition for #{name} in #{cluster_path}") end
102
+
103
+ @@realms[name]
104
+ end
105
+
94
106
  #
95
107
  # Return cluster if it's defined. Otherwise, search Ironfan.cluster_path
96
108
  # for an eponymous file, load it, and return the cluster it defines.
@@ -104,16 +116,21 @@ module Ironfan
104
116
  raise ArgumentError, "Please supply a cluster name" if name.to_s.empty?
105
117
  return @@clusters[name] if @@clusters[name]
106
118
 
119
+ load_cluster_files
120
+
121
+ unless @@clusters[name] then die("Couldn't find a cluster definition for #{name} in #{cluster_path}") end
122
+
123
+ @@clusters[name]
124
+ end
125
+
126
+ def self.load_cluster_files
107
127
  cluster_path.each do |cp_dir|
108
128
  Dir[ File.join(cp_dir, '*.rb') ].each do |filename|
109
129
  Chef::Log.info("Loading cluster file #{filename}")
110
130
  require filename
131
+ clusters.values.each{|cluster| cluster.source_file ||= filename}
111
132
  end
112
133
  end
113
-
114
- unless @@clusters[name] then die("Couldn't find a cluster definition for #{name} in #{cluster_path}") end
115
-
116
- @@clusters[name]
117
134
  end
118
135
 
119
136
  #