bosh_cli 0.16

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.
Files changed (113) hide show
  1. data/README +4 -0
  2. data/Rakefile +55 -0
  3. data/bin/bosh +17 -0
  4. data/lib/cli.rb +76 -0
  5. data/lib/cli/cache.rb +44 -0
  6. data/lib/cli/changeset_helper.rb +142 -0
  7. data/lib/cli/command_definition.rb +52 -0
  8. data/lib/cli/commands/base.rb +245 -0
  9. data/lib/cli/commands/biff.rb +300 -0
  10. data/lib/cli/commands/blob.rb +125 -0
  11. data/lib/cli/commands/cloudcheck.rb +169 -0
  12. data/lib/cli/commands/deployment.rb +147 -0
  13. data/lib/cli/commands/job.rb +42 -0
  14. data/lib/cli/commands/job_management.rb +117 -0
  15. data/lib/cli/commands/log_management.rb +81 -0
  16. data/lib/cli/commands/maintenance.rb +131 -0
  17. data/lib/cli/commands/misc.rb +240 -0
  18. data/lib/cli/commands/package.rb +112 -0
  19. data/lib/cli/commands/property_management.rb +125 -0
  20. data/lib/cli/commands/release.rb +469 -0
  21. data/lib/cli/commands/ssh.rb +271 -0
  22. data/lib/cli/commands/stemcell.rb +184 -0
  23. data/lib/cli/commands/task.rb +213 -0
  24. data/lib/cli/commands/user.rb +28 -0
  25. data/lib/cli/commands/vms.rb +53 -0
  26. data/lib/cli/config.rb +154 -0
  27. data/lib/cli/core_ext.rb +145 -0
  28. data/lib/cli/dependency_helper.rb +62 -0
  29. data/lib/cli/deployment_helper.rb +263 -0
  30. data/lib/cli/deployment_manifest_compiler.rb +28 -0
  31. data/lib/cli/director.rb +633 -0
  32. data/lib/cli/director_task.rb +64 -0
  33. data/lib/cli/errors.rb +48 -0
  34. data/lib/cli/event_log_renderer.rb +351 -0
  35. data/lib/cli/job_builder.rb +226 -0
  36. data/lib/cli/package_builder.rb +254 -0
  37. data/lib/cli/packaging_helper.rb +248 -0
  38. data/lib/cli/release.rb +176 -0
  39. data/lib/cli/release_builder.rb +215 -0
  40. data/lib/cli/release_compiler.rb +178 -0
  41. data/lib/cli/release_tarball.rb +272 -0
  42. data/lib/cli/runner.rb +771 -0
  43. data/lib/cli/stemcell.rb +83 -0
  44. data/lib/cli/task_log_renderer.rb +40 -0
  45. data/lib/cli/templates/help_message.erb +75 -0
  46. data/lib/cli/validation.rb +42 -0
  47. data/lib/cli/version.rb +7 -0
  48. data/lib/cli/version_calc.rb +48 -0
  49. data/lib/cli/versions_index.rb +126 -0
  50. data/lib/cli/yaml_helper.rb +62 -0
  51. data/spec/assets/biff/bad_gateway_config.yml +28 -0
  52. data/spec/assets/biff/good_simple_config.yml +63 -0
  53. data/spec/assets/biff/good_simple_golden_config.yml +63 -0
  54. data/spec/assets/biff/good_simple_template.erb +69 -0
  55. data/spec/assets/biff/multiple_subnets_config.yml +40 -0
  56. data/spec/assets/biff/network_only_template.erb +34 -0
  57. data/spec/assets/biff/no_cc_config.yml +27 -0
  58. data/spec/assets/biff/no_range_config.yml +27 -0
  59. data/spec/assets/biff/no_subnet_config.yml +16 -0
  60. data/spec/assets/biff/ok_network_config.yml +30 -0
  61. data/spec/assets/biff/properties_template.erb +6 -0
  62. data/spec/assets/deployment.MF +0 -0
  63. data/spec/assets/plugins/bosh/cli/commands/echo.rb +43 -0
  64. data/spec/assets/plugins/bosh/cli/commands/ruby.rb +24 -0
  65. data/spec/assets/release/jobs/cacher.tgz +0 -0
  66. data/spec/assets/release/jobs/cacher/config/file1.conf +0 -0
  67. data/spec/assets/release/jobs/cacher/config/file2.conf +0 -0
  68. data/spec/assets/release/jobs/cacher/job.MF +6 -0
  69. data/spec/assets/release/jobs/cacher/monit +1 -0
  70. data/spec/assets/release/jobs/cleaner.tgz +0 -0
  71. data/spec/assets/release/jobs/cleaner/job.MF +4 -0
  72. data/spec/assets/release/jobs/cleaner/monit +1 -0
  73. data/spec/assets/release/jobs/sweeper.tgz +0 -0
  74. data/spec/assets/release/jobs/sweeper/config/test.conf +1 -0
  75. data/spec/assets/release/jobs/sweeper/job.MF +5 -0
  76. data/spec/assets/release/jobs/sweeper/monit +1 -0
  77. data/spec/assets/release/packages/mutator.tar.gz +0 -0
  78. data/spec/assets/release/packages/stuff.tgz +0 -0
  79. data/spec/assets/release/release.MF +17 -0
  80. data/spec/assets/release_invalid_checksum.tgz +0 -0
  81. data/spec/assets/release_invalid_jobs.tgz +0 -0
  82. data/spec/assets/release_no_name.tgz +0 -0
  83. data/spec/assets/release_no_version.tgz +0 -0
  84. data/spec/assets/stemcell/image +1 -0
  85. data/spec/assets/stemcell/stemcell.MF +6 -0
  86. data/spec/assets/stemcell_invalid_mf.tgz +0 -0
  87. data/spec/assets/stemcell_no_image.tgz +0 -0
  88. data/spec/assets/valid_release.tgz +0 -0
  89. data/spec/assets/valid_stemcell.tgz +0 -0
  90. data/spec/spec_helper.rb +25 -0
  91. data/spec/unit/base_command_spec.rb +66 -0
  92. data/spec/unit/biff_spec.rb +135 -0
  93. data/spec/unit/cache_spec.rb +36 -0
  94. data/spec/unit/cli_commands_spec.rb +481 -0
  95. data/spec/unit/config_spec.rb +139 -0
  96. data/spec/unit/core_ext_spec.rb +77 -0
  97. data/spec/unit/dependency_helper_spec.rb +52 -0
  98. data/spec/unit/deployment_manifest_compiler_spec.rb +63 -0
  99. data/spec/unit/director_spec.rb +511 -0
  100. data/spec/unit/director_task_spec.rb +48 -0
  101. data/spec/unit/event_log_renderer_spec.rb +171 -0
  102. data/spec/unit/hash_changeset_spec.rb +73 -0
  103. data/spec/unit/job_builder_spec.rb +454 -0
  104. data/spec/unit/package_builder_spec.rb +567 -0
  105. data/spec/unit/release_builder_spec.rb +65 -0
  106. data/spec/unit/release_spec.rb +66 -0
  107. data/spec/unit/release_tarball_spec.rb +33 -0
  108. data/spec/unit/runner_spec.rb +140 -0
  109. data/spec/unit/ssh_spec.rb +78 -0
  110. data/spec/unit/stemcell_spec.rb +17 -0
  111. data/spec/unit/version_calc_spec.rb +27 -0
  112. data/spec/unit/versions_index_spec.rb +132 -0
  113. metadata +338 -0
@@ -0,0 +1,147 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli::Command
4
+ class Deployment < Base
5
+ include Bosh::Cli::DeploymentHelper
6
+
7
+ def show_current
8
+ say(deployment ?
9
+ "Current deployment is '#{deployment.green}'" :
10
+ "Deployment not set")
11
+ end
12
+
13
+ def set_current(name)
14
+ manifest_filename = find_deployment(name)
15
+
16
+ unless File.exists?(manifest_filename)
17
+ err("Missing manifest for #{name} (tried '#{manifest_filename}')")
18
+ end
19
+
20
+ manifest = load_yaml_file(manifest_filename)
21
+
22
+ unless manifest.is_a?(Hash)
23
+ err("Invalid manifest format")
24
+ end
25
+
26
+ unless manifest["target"].blank?
27
+ err(manifest_target_upgrade_notice)
28
+ end
29
+
30
+ if manifest["director_uuid"].blank?
31
+ err("Director UUID is not defined in deployment manifest")
32
+ end
33
+
34
+ if target
35
+ old_director = Bosh::Cli::Director.new(target, username, password)
36
+ old_director_uuid = old_director.get_status["uuid"] rescue nil
37
+ else
38
+ old_director_uuid = nil
39
+ end
40
+
41
+ if old_director_uuid != manifest["director_uuid"]
42
+ new_target_url = config.resolve_alias(:target,
43
+ manifest["director_uuid"])
44
+ if new_target_url.blank?
45
+ err("Cannot find director url for " +
46
+ "UUID '#{manifest["director_uuid"]}'")
47
+ end
48
+
49
+ new_director = Bosh::Cli::Director.new(new_target_url,
50
+ username, password)
51
+ status = new_director.get_status
52
+
53
+ config.target = new_target_url
54
+ config.target_name = status["name"]
55
+ config.target_version = status["version"]
56
+ config.target_uuid = status["uuid"]
57
+ say("#{"WARNING!".red} Your target has been" +
58
+ "changed to `#{target.red}'!")
59
+ end
60
+
61
+ say("Deployment set to '#{manifest_filename.green}'")
62
+ config.set_deployment(manifest_filename)
63
+ config.save
64
+ end
65
+
66
+ def perform(*options)
67
+ auth_required
68
+ recreate = options.include?("--recreate")
69
+
70
+ manifest_yaml = prepare_deployment_manifest(:yaml => true,
71
+ :resolve_properties => true)
72
+
73
+ if interactive?
74
+ inspect_deployment_changes(YAML.load(manifest_yaml))
75
+ say("Please review all changes carefully".yellow)
76
+ end
77
+
78
+ desc = "`#{File.basename(deployment).green}' to `#{target_name.green}'"
79
+
80
+ unless confirmed?("Deploying #{desc}")
81
+ cancel_deployment
82
+ end
83
+
84
+ status, body = director.deploy(manifest_yaml, :recreate => recreate)
85
+
86
+ responses = {
87
+ :done => "Deployed #{desc}",
88
+ :non_trackable => "Started deployment but director at `#{target}' " +
89
+ "doesn't support deployment tracking",
90
+ :track_timeout => "Started deployment but timed out out " +
91
+ "while tracking status",
92
+ :error => "Started deployment but received an error " +
93
+ "while tracking status",
94
+ :invalid => "Deployment is invalid, please fix it and deploy again"
95
+ }
96
+
97
+ say(responses[status] || "Cannot deploy: #{body}")
98
+ end
99
+
100
+ def delete(name, *options)
101
+ auth_required
102
+ force = options.include?("--force")
103
+
104
+ say("\nYou are going to delete deployment `#{name}'.\n\n")
105
+ say("THIS IS A VERY DESTRUCTIVE OPERATION AND IT CANNOT BE UNDONE!\n".red)
106
+
107
+ unless confirmed?
108
+ say("Canceled deleting deployment".green)
109
+ return
110
+ end
111
+
112
+ status, message = director.delete_deployment(name, :force => force)
113
+
114
+ responses = {
115
+ :done => "Deleted deployment '#{name}'",
116
+ :non_trackable => "Deployment delete in progress but director " +
117
+ "at '#{target}' doesn't support task tracking",
118
+ :track_timeout => "Timed out out while tracking deployment " +
119
+ "deletion progress",
120
+ :error => "Attempted to delete deployment but received " +
121
+ "an error while tracking status",
122
+ }
123
+
124
+ say(responses[status] || "Cannot delete deployment: #{message}")
125
+ end
126
+
127
+ def list
128
+ auth_required
129
+
130
+ deployments = director.list_deployments
131
+
132
+ err("No deployments") if deployments.size == 0
133
+
134
+ deployments_table = table do |t|
135
+ t.headings = ["Name"]
136
+ deployments.each do |r|
137
+ t << [r["name"]]
138
+ end
139
+ end
140
+
141
+ say("\n")
142
+ say(deployments_table)
143
+ say("\n")
144
+ say("Deployments total: %d" % deployments.size)
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,42 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli::Command
4
+ class Job < Base
5
+
6
+ def generate(name)
7
+ check_if_release_dir
8
+
9
+ unless name.bosh_valid_id?
10
+ err("`#{name}' is not a vaild BOSH id")
11
+ end
12
+
13
+ job_dir = File.join("jobs", name)
14
+
15
+ if File.exists?(job_dir)
16
+ err("Job `#{name}' already exists, please pick another name")
17
+ end
18
+
19
+ say("create\t#{job_dir}")
20
+ FileUtils.mkdir_p(job_dir)
21
+
22
+ templates_dir = File.join(job_dir, "templates")
23
+ say("create\t#{templates_dir}")
24
+ FileUtils.mkdir_p(templates_dir)
25
+
26
+ spec_file = File.join(job_dir, "spec")
27
+ say("create\t#{spec_file}")
28
+ FileUtils.touch(spec_file)
29
+
30
+ monit_file = File.join(job_dir, "monit")
31
+ say("create\t#{monit_file}")
32
+ FileUtils.touch(monit_file)
33
+
34
+ File.open(spec_file, "w") do |f|
35
+ f.write("---\nname: #{name}\ntemplates:\n\npackages:\n")
36
+ end
37
+
38
+ say("\nGenerated skeleton for `#{name}' job in `#{job_dir}'")
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,117 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli::Command
4
+ class JobManagement < Base
5
+ include Bosh::Cli::DeploymentHelper
6
+
7
+ def start_job(*args)
8
+ change_job_state(:start, *args)
9
+ end
10
+
11
+ def stop_job(*args)
12
+ change_job_state(:stop, *args)
13
+ end
14
+
15
+ def restart_job(*args)
16
+ change_job_state(:restart, *args)
17
+ end
18
+
19
+ def recreate_job(*args)
20
+ change_job_state(:recreate, *args)
21
+ end
22
+
23
+ def change_job_state(operation, *args)
24
+ auth_required
25
+ manifest_yaml = prepare_deployment_manifest(:yaml => true)
26
+ manifest = YAML.load(manifest_yaml)
27
+
28
+ unless [:start, :stop, :restart, :recreate].include?(operation)
29
+ err("Unknown operation `#{operation}': supported operations are " +
30
+ "`start', `stop', `restart', `recreate'")
31
+ end
32
+
33
+ args = args.dup
34
+ hard = args.delete("--hard")
35
+ soft = args.delete("--soft")
36
+ force = args.delete("--force")
37
+
38
+ if hard && soft
39
+ err("Cannot handle both --hard and --soft options, please choose one")
40
+ end
41
+
42
+ if operation != :stop && (hard || soft)
43
+ err("--hard and --soft options only make sense for `stop' operation")
44
+ end
45
+
46
+ job = args.shift
47
+ index = args.shift
48
+ deployment_desc = "`#{deployment.green}' to `#{target_name.green}'"
49
+ job_desc = index ? "#{job}(#{index})" : "#{job}"
50
+
51
+ case operation
52
+ when :start
53
+ op_desc = "start #{job_desc}"
54
+ new_state = "started"
55
+ completion_desc = "#{job_desc.green} has been started"
56
+ when :stop
57
+ if hard
58
+ op_desc = "stop #{job_desc} and power off its VM(s)"
59
+ completion_desc = "#{job_desc.green} has been stopped, " +
60
+ "VM(s) powered off"
61
+ new_state = "detached"
62
+ else
63
+ op_desc = "stop #{job_desc}"
64
+ completion_desc = "#{job_desc.green} has been stopped, " +
65
+ "VM(s) still running"
66
+ new_state = "stopped"
67
+ end
68
+ when :restart
69
+ op_desc = "restart #{job_desc}"
70
+ new_state = "restart"
71
+ completion_desc = "#{job_desc.green} has been restarted"
72
+ when :recreate
73
+ op_desc = "recreate #{job_desc}"
74
+ new_state = "recreate"
75
+ completion_desc = "#{job_desc.green} has been recreated"
76
+ end
77
+
78
+ say("You are about to #{op_desc.green}")
79
+
80
+ if interactive?
81
+ # TODO: refactor inspect_deployment_changes
82
+ # to decouple changeset structure and rendering
83
+ other_changes_present = inspect_deployment_changes(
84
+ manifest, :show_empty_changeset => false)
85
+
86
+ if other_changes_present && !force
87
+ err("Cannot perform job management when other deployment changes " +
88
+ "are present. Please use `--force' to override.")
89
+ end
90
+ unless confirmed?("#{op_desc.capitalize}?")
91
+ cancel_deployment
92
+ end
93
+ end
94
+ nl
95
+
96
+ say("Performing `#{op_desc}'...")
97
+
98
+ status, body = director.change_job_state(manifest["name"],
99
+ manifest_yaml,
100
+ job, index, new_state)
101
+
102
+ responses = {
103
+ :done => completion_desc,
104
+ :non_trackable => "Started deployment but director at '#{target}' " +
105
+ "doesn't support deployment tracking",
106
+ :track_timeout => "Started deployment but timed out out "+
107
+ "while tracking status",
108
+ :error => "Started deployment but received an error " +
109
+ "while tracking status",
110
+ :invalid => "Deployment is invalid, please fix it and deploy again"
111
+ }
112
+
113
+ say(responses[status] || "Cannot deploy: #{body}")
114
+ end
115
+
116
+ end
117
+ end
@@ -0,0 +1,81 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli::Command
4
+ class LogManagement < Base
5
+ include Bosh::Cli::DeploymentHelper
6
+
7
+ def fetch_logs(*args)
8
+ auth_required
9
+ target_required
10
+
11
+ job = args.shift
12
+ index = args.shift
13
+ filters = nil
14
+
15
+ for_job = args.delete("--job")
16
+ for_agent = args.delete("--agent")
17
+
18
+ if for_job && for_agent
19
+ err("Please specify which logs you want, job or agent")
20
+ elsif for_agent
21
+ log_type = "agent"
22
+ else # default log type is 'job'
23
+ log_type = "job"
24
+ end
25
+
26
+ if args.include?("--only")
27
+ pos = args.index("--only")
28
+ filters = args[pos+1]
29
+ if filters.nil?
30
+ err("Please provide a list of filters separated by comma")
31
+ end
32
+ args.delete("--only")
33
+ args.delete(filters)
34
+ elsif args.include?("--all")
35
+ args.delete("--all")
36
+ filters = "all"
37
+ end
38
+
39
+ if for_agent && !filters.nil? && filters != "all"
40
+ err("Custom filtering is not supported for agent logs")
41
+ end
42
+
43
+ if index !~ /^\d+$/
44
+ err("Job index is expected to be a positive integer")
45
+ end
46
+
47
+ if args.size > 0
48
+ err("Unknown arguments: #{args.join(", ")}")
49
+ end
50
+
51
+ manifest = prepare_deployment_manifest
52
+
53
+ resource_id = director.fetch_logs(manifest["name"], job, index,
54
+ log_type, filters)
55
+
56
+ if resource_id.nil?
57
+ err("Error retrieving logs")
58
+ end
59
+
60
+ nl
61
+ say("Downloading log bundle (#{resource_id.to_s.green})...")
62
+
63
+ begin
64
+ time = Time.now.strftime("%Y-%m-%d@%H-%M-%S")
65
+ log_file = File.join(Dir.pwd, "#{job}.#{index}.#{time}.tgz")
66
+
67
+ tmp_file = director.download_resource(resource_id)
68
+
69
+ FileUtils.mv(tmp_file, log_file)
70
+ say("Logs saved in `#{log_file.green}'")
71
+ rescue Bosh::Cli::DirectorError => e
72
+ err("Unable to download logs from director: #{e}")
73
+ ensure
74
+ FileUtils.rm_rf(tmp_file) if File.exists?(tmp_file)
75
+ end
76
+
77
+ end
78
+
79
+ end
80
+ end
81
+
@@ -0,0 +1,131 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Cli::Command
4
+ class Maintenance < Base
5
+ include Bosh::Cli::VersionCalc
6
+
7
+ RELEASES_TO_KEEP = 2
8
+ STEMCELLS_TO_KEEP = 2
9
+
10
+ def cleanup
11
+ target_required
12
+ auth_required
13
+
14
+ releases_to_keep = RELEASES_TO_KEEP
15
+ stemcells_to_keep = STEMCELLS_TO_KEEP
16
+
17
+ release_wording = pluralize(releases_to_keep, "latest version")
18
+ stemcell_wording = pluralize(stemcells_to_keep, "latest version")
19
+
20
+ desc = <<-EOS.gsub(/^ */, "")
21
+ Cleanup command will attempt to delete old unused
22
+ release versions and stemcells from your currently
23
+ targeted director at #{target_name.green}.
24
+
25
+ Only #{release_wording.green} of each release
26
+ and #{stemcell_wording.green} of each stemcell will be kept.
27
+
28
+ Releases and stemcells that are in use will not be affected.
29
+ EOS
30
+
31
+ say("\n#{desc}\n")
32
+
33
+ err("Cleanup canceled") unless confirmed?
34
+
35
+ nl
36
+ cleanup_stemcells(stemcells_to_keep)
37
+ nl
38
+ cleanup_releases(releases_to_keep)
39
+
40
+ nl
41
+ say("Cleanup complete".green)
42
+ end
43
+
44
+ private
45
+
46
+ def cleanup_stemcells(n_to_keep)
47
+ stemcells_by_name = director.list_stemcells.inject({}) do |h, stemcell|
48
+ h[stemcell["name"]] ||= []
49
+ h[stemcell["name"]] << stemcell
50
+ h
51
+ end
52
+
53
+ delete_list = []
54
+ say("Deleting old stemcells")
55
+
56
+ stemcells_by_name.each_pair do |name, stemcells|
57
+ stemcells.sort! do |sc1, sc2|
58
+ version_cmp(sc1["version"], sc2["version"])
59
+ end
60
+ delete_list += stemcells[0...(-n_to_keep)]
61
+ end
62
+
63
+ if delete_list.size > 0
64
+ delete_list.each do |stemcell|
65
+ name, version = stemcell["name"], stemcell["version"]
66
+ desc = "#{name}/#{version}"
67
+ perform(desc) do
68
+ director.delete_stemcell(name, version, :quiet => true)
69
+ end
70
+ end
71
+ else
72
+ say(" none found".yellow)
73
+ end
74
+ end
75
+
76
+ def cleanup_releases(n_to_keep)
77
+ delete_list = []
78
+ say("Deleting old release versions")
79
+
80
+ director.list_releases.each do |release|
81
+ name = release["name"]
82
+ versions = release["versions"].sort { |v1, v2| version_cmp(v1, v2) }
83
+
84
+ versions[0...(-n_to_keep)].each do |version|
85
+ delete_list << [name, version]
86
+ end
87
+ end
88
+
89
+ if delete_list.size > 0
90
+ delete_list.each do |name, version|
91
+ desc = "#{name}/#{version}"
92
+ perform(desc) do
93
+ director.delete_release(name, :force => false,
94
+ :version => version, :quiet => true)
95
+ end
96
+ end
97
+ else
98
+ say(" none found".yellow)
99
+ end
100
+ end
101
+
102
+ def refresh(message)
103
+ say("\r", "")
104
+ say(" " * 80, "")
105
+ say("\r#{message}", "")
106
+ end
107
+
108
+ def perform(desc)
109
+ say(" #{desc.yellow.ljust(40)}", "")
110
+ say("IN PROGRESS...".yellow, "")
111
+
112
+ status, task_id = yield
113
+ responses = {
114
+ :done => "DELETED".green,
115
+ :non_trackable => "CANNOT TRACK".red,
116
+ :track_timeout => "TIMED OUT".red,
117
+ :error => "ERROR".red,
118
+ }
119
+
120
+ refresh(" #{desc.yellow.ljust(40)}#{responses[status]}\n")
121
+
122
+ if status == :error
123
+ task = director.get_task(task_id)
124
+ say(" #{task["result"].red}")
125
+ end
126
+
127
+ status == :done
128
+ end
129
+
130
+ end
131
+ end