vanagon 0.15.36 → 0.17.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -23
  3. data/bin/build +4 -25
  4. data/bin/build_host_info +4 -17
  5. data/bin/build_requirements +4 -31
  6. data/bin/inspect +4 -21
  7. data/bin/render +4 -22
  8. data/bin/ship +4 -28
  9. data/bin/sign +4 -11
  10. data/bin/vanagon +7 -0
  11. data/lib/vanagon.rb +1 -1
  12. data/lib/vanagon/cli.rb +94 -0
  13. data/lib/vanagon/cli/build.rb +75 -0
  14. data/lib/vanagon/cli/build_host_info.rb +49 -0
  15. data/lib/vanagon/cli/build_requirements.rb +60 -0
  16. data/lib/vanagon/cli/inspect.rb +65 -0
  17. data/lib/vanagon/cli/render.rb +51 -0
  18. data/lib/vanagon/cli/ship.rb +52 -0
  19. data/lib/vanagon/cli/sign.rb +34 -0
  20. data/lib/vanagon/driver.rb +11 -7
  21. data/lib/vanagon/engine/always_be_scheduling.rb +271 -1
  22. data/lib/vanagon/engine/docker.rb +101 -14
  23. data/lib/vanagon/engine/pooler.rb +7 -3
  24. data/lib/vanagon/platform.rb +5 -3
  25. data/lib/vanagon/platform/deb.rb +1 -1
  26. data/lib/vanagon/platform/dsl.rb +11 -0
  27. data/lib/vanagon/platform/rpm.rb +1 -1
  28. data/lib/vanagon/platform/windows.rb +29 -2
  29. data/lib/vanagon/project.rb +23 -4
  30. data/lib/vanagon/project/dsl.rb +33 -0
  31. data/lib/vanagon/utilities.rb +30 -8
  32. data/resources/rpm/project.spec.erb +3 -0
  33. data/spec/lib/vanagon/cli_spec.rb +80 -0
  34. data/spec/lib/vanagon/engine/always_be_scheduling_spec.rb +113 -1
  35. data/spec/lib/vanagon/engine/docker_spec.rb +74 -16
  36. data/spec/lib/vanagon/engine/ec2_spec.rb +2 -0
  37. data/spec/lib/vanagon/engine/pooler_spec.rb +1 -1
  38. data/spec/spec_helper.rb +1 -0
  39. metadata +56 -33
  40. data/lib/vanagon/optparse.rb +0 -86
  41. data/spec/lib/vanagon/optparse_spec.rb +0 -64
@@ -0,0 +1,75 @@
1
+ require 'docopt'
2
+
3
+ class Vanagon
4
+ class CLI
5
+ class Build < Vanagon::CLI
6
+ DOCUMENTATION = <<~DOCOPT.freeze
7
+ Usage:
8
+ build [options] <project-name> <platforms> [<targets>]
9
+
10
+ Options:
11
+ -h, --help Display help
12
+ -c, --configdir DIRECTORY Configuration directory [default: #{Dir.pwd}/configs]
13
+ -e, --engine ENGINE Custom engine to use [base, local, docker, pooler] [default: pooler]
14
+ -o, --only-build COMPONENT,COMPONENT,...
15
+ Only build listed COMPONENTs
16
+ -p, --preserve [RULE] Rule for VM preservation: never, on-failure, always
17
+ [Default: always]
18
+ -r, --remote-workdir DIRECTORY Working directory on the remote host
19
+ -s, --skipcheck Skip the "check" stage when building components
20
+ -w, --workdir DIRECTORY Working directory on the local host
21
+ -v, --verbose Only here for backwards compatibility. Does nothing.
22
+ DOCOPT
23
+
24
+ def parse(argv)
25
+ Docopt.docopt(DOCUMENTATION, { argv: argv })
26
+ rescue Docopt::Exit => e
27
+ puts e.message
28
+ exit 1
29
+ end
30
+
31
+ def run(options) # rubocop:disable Metrics/AbcSize
32
+ project = options[:project_name]
33
+ platform_list = options[:platforms].split(',')
34
+ target_list = []
35
+ unless options[:targets].nil? || options[:targets].empty?
36
+ target_list = options[:targets].split(',')
37
+ end
38
+
39
+ platform_list.zip(target_list).each do |pair|
40
+ platform, target = pair
41
+ artifact = Vanagon::Driver.new(platform, project, options.merge({ 'target' => target }))
42
+ artifact.run
43
+ end
44
+ end
45
+
46
+ def options_translate(docopt_options)
47
+ translations = {
48
+ '--verbose' => :verbose,
49
+ '--workdir' => :workdir,
50
+ '--remote-workdir' => :"remote-workdir",
51
+ '--configdir' => :configdir,
52
+ '--engine' => :engine,
53
+ '--skipcheck' => :skipcheck,
54
+ '--preserve' => :preserve,
55
+ '--only-build' => :only_build,
56
+ '<project-name>' => :project_name,
57
+ '<platforms>' => :platforms,
58
+ '<targets>' => :targets
59
+ }
60
+ return docopt_options.map { |k, v| [translations[k], v] }.to_h
61
+ end
62
+
63
+ def options_validate(options)
64
+ # Handle --preserve option checking
65
+ valid_preserves = %w[always never on-failure]
66
+ unless valid_preserves.include? options[:preserve]
67
+ raise InvalidArgument, "--preserve option can only be one of: " +
68
+ valid_preserves.join(', ')
69
+ end
70
+ options[:preserve] = options[:preserve].to_sym
71
+ return options
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,49 @@
1
+ require 'docopt'
2
+
3
+ class Vanagon
4
+ class CLI
5
+ class BuildHostInfo < Vanagon::CLI
6
+ DOCUMENTATION = <<~DOCOPT.freeze
7
+ Usage:
8
+ build_host_info [options] <project-name> <platforms>
9
+
10
+ Options:
11
+ -h, --help Display help
12
+ -c, --configdir DIRECTORY Configuration directory [default: #{Dir.pwd}/configs]
13
+ -e, --engine ENGINE Custom engine to use [base, local, docker, pooler] [default: pooler]
14
+ -w, --workdir DIRECTORY Working directory on the local host
15
+ -v, --verbose Only here for backwards compatibility. Does nothing.
16
+ DOCOPT
17
+
18
+ def parse(argv)
19
+ Docopt.docopt(DOCUMENTATION, { argv: argv })
20
+ rescue Docopt::Exit => e
21
+ puts e.message
22
+ exit 1
23
+ end
24
+
25
+ def run(options)
26
+ platforms = options[:platforms].split(',')
27
+ project = options[:project_name]
28
+
29
+ platforms.each do |platform|
30
+ driver = Vanagon::Driver.new(platform, project, options)
31
+ $stdout.puts JSON.generate(driver.build_host_info)
32
+ end
33
+ end
34
+
35
+ def options_translate(docopt_options)
36
+ translations = {
37
+ '--verbose' => :verbose,
38
+ '--workdir' => :workdir,
39
+ '--configdir' => :configdir,
40
+ '--engine' => :engine,
41
+ '<project-name>' => :project_name,
42
+ '<platforms>' => :platforms,
43
+ '<targets>' => :targets
44
+ }
45
+ return docopt_options.map { |k, v| [translations[k], v] }.to_h
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,60 @@
1
+ require 'docopt'
2
+ require 'json'
3
+
4
+ class Vanagon
5
+ class CLI
6
+ class BuildRequirements < Vanagon::CLI
7
+ DOCUMENTATION = <<~DOCOPT.freeze
8
+ Usage:
9
+ build_requirements [options] <project-name> <platform>
10
+
11
+ Options:
12
+ -h, --help Display help
13
+ -c, --configdir DIRECTORY Configuration directory [default: #{Dir.pwd}/configs]
14
+ -e, --engine ENGINE Custom engine to use [base, local, docker, pooler] [default: pooler]
15
+ -w, --workdir DIRECTORY Working directory on the local host
16
+ -v, --verbose Only here for backwards compatibility. Does nothing.
17
+ DOCOPT
18
+
19
+ def parse(argv)
20
+ Docopt.docopt(DOCUMENTATION, { argv: argv })
21
+ rescue Docopt::Exit => e
22
+ puts e.message
23
+ exit 1
24
+ end
25
+
26
+ def run(options) # rubocop:disable Metrics/AbcSize
27
+ platform = options[:platform]
28
+ project = options[:project_name]
29
+ driver = Vanagon::Driver.new(platform, project)
30
+
31
+ components = driver.project.components
32
+ component_names = components.map(&:name)
33
+ build_requirements = []
34
+ components.each do |component|
35
+ build_requirements << component.build_requires.reject do |requirement|
36
+ # only include external requirements: i.e. those that do not match
37
+ # other components in the project
38
+ component_names.include?(requirement)
39
+ end
40
+ end
41
+
42
+ $stdout.puts
43
+ $stdout.puts "**** External packages required to build #{project} on #{platform}: ***"
44
+ $stdout.puts JSON.pretty_generate(build_requirements.flatten.uniq.sort)
45
+ end
46
+
47
+ def options_translate(docopt_options)
48
+ translations = {
49
+ '--verbose' => :verbose,
50
+ '--workdir' => :workdir,
51
+ '--configdir' => :configdir,
52
+ '--engine' => :engine,
53
+ '<project-name>' => :project_name,
54
+ '<platform>' => :platform,
55
+ }
56
+ return docopt_options.map { |k, v| [translations[k], v] }.to_h
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,65 @@
1
+ require 'docopt'
2
+ require 'json'
3
+
4
+ class Vanagon
5
+ class CLI
6
+ class Inspect < Vanagon::CLI
7
+ DOCUMENTATION = <<~DOCOPT.freeze
8
+ Usage:
9
+ inspect [options] <project-name> <platforms>
10
+
11
+ Options:
12
+ -h, --help Display help
13
+ -c, --configdir DIRECTORY Configuration directory [default: #{Dir.pwd}/configs]
14
+ -e, --engine ENGINE Custom engine to use [base, local, docker, pooler] [default: pooler]
15
+
16
+ -p, --preserve [RULE] Rule for VM preservation: never, on-failure, always
17
+ [Default: on-failure]
18
+ -w, --workdir DIRECTORY Working directory on the local host
19
+ -v, --verbose Only here for backwards compatibility. Does nothing.
20
+ DOCOPT
21
+
22
+ def parse(argv)
23
+ Docopt.docopt(DOCUMENTATION, { argv: argv })
24
+ rescue Docopt::Exit => e
25
+ puts e.message
26
+ exit 1
27
+ end
28
+
29
+ def run(options)
30
+ platforms = options[:platforms].split(',')
31
+ project = options[:project_name]
32
+
33
+ platforms.each do |platform|
34
+ driver = Vanagon::Driver.new(platform, project, options)
35
+ components = driver.project.components.map(&:to_hash)
36
+ $stdout.puts JSON.pretty_generate(components)
37
+ end
38
+ end
39
+
40
+ def options_translate(docopt_options)
41
+ translations = {
42
+ '--verbose' => :verbose,
43
+ '--workdir' => :workdir,
44
+ '--configdir' => :configdir,
45
+ '--engine' => :engine,
46
+ '--preserve' => :preserve,
47
+ '<project-name>' => :project_name,
48
+ '<platforms>' => :platforms
49
+ }
50
+ return docopt_options.map { |k, v| [translations[k], v] }.to_h
51
+ end
52
+
53
+ def options_validate(options)
54
+ # Handle --preserve option checking
55
+ valid_preserves = %w[always never on-failure]
56
+ unless valid_preserves.include? options[:preserve]
57
+ raise InvalidArgument, "--preserve option can only be one of: " +
58
+ valid_preserves.join(', ')
59
+ end
60
+ options[:preserve] = options[:preserve].to_sym
61
+ return options
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,51 @@
1
+ require 'docopt'
2
+ require 'json'
3
+
4
+ class Vanagon
5
+ class CLI
6
+ class Render < Vanagon::CLI
7
+ DOCUMENTATION = <<~DOCOPT.freeze
8
+ Usage:
9
+ render [options] <project-name> <platforms>
10
+
11
+ Options:
12
+ -h, --help Display help
13
+ -c, --configdir DIRECTORY Configuration directory [default: #{Dir.pwd}/configs]
14
+ -e, --engine ENGINE Custom engine to use [base, local, docker, pooler] [default: pooler]
15
+ -w, --workdir DIRECTORY Working directory on the local host
16
+ -v, --verbose Only here for backwards compatibility. Does nothing.
17
+ DOCOPT
18
+
19
+ def parse(argv)
20
+ Docopt.docopt(DOCUMENTATION, { argv: argv })
21
+ rescue Docopt::Exit => e
22
+ puts e.message
23
+ exit 1
24
+ end
25
+
26
+ def run(options)
27
+ platforms = options[:platforms].split(',')
28
+ project = options[:project_name]
29
+ target_list = []
30
+
31
+ platforms.zip(target_list).each do |pair|
32
+ platform, target = pair
33
+ artifact = Vanagon::Driver.new(platform, project, options.merge({ :target => target }))
34
+ artifact.render
35
+ end
36
+ end
37
+
38
+ def options_translate(docopt_options)
39
+ translations = {
40
+ '--verbose' => :verbose,
41
+ '--workdir' => :workdir,
42
+ '--configdir' => :configdir,
43
+ '--engine' => :engine,
44
+ '<project-name>' => :project_name,
45
+ '<platforms>' => :platforms,
46
+ }
47
+ return docopt_options.map { |k, v| [translations[k], v] }.to_h
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,52 @@
1
+ require 'docopt'
2
+
3
+ class Vanagon
4
+ class CLI
5
+ class Ship < Vanagon::CLI
6
+ DOCUMENTATION = <<~DOCOPT.freeze
7
+ Usage:
8
+ ship [--help]
9
+
10
+ Options:
11
+ -h, --help Display help
12
+ DOCOPT
13
+
14
+ def parse(argv)
15
+ Docopt.docopt(DOCUMENTATION, { argv: argv })
16
+ rescue Docopt::Exit => e
17
+ puts e.message
18
+ exit 1
19
+ end
20
+
21
+ def run(_)
22
+ ENV['PROJECT_ROOT'] = Dir.pwd
23
+
24
+ artifactory_warning = <<-DOC
25
+ Unable to ship packages to artifactory. Please make sure you are pointing to a
26
+ recent version of packaging in your Gemfile. Please also make sure you include
27
+ the artifactory gem in your Gemfile.
28
+
29
+ Examples:
30
+ gem 'packaging', :github => 'puppetlabs/packaging', branch: '1.0.x'
31
+ gem 'artifactory'
32
+ DOC
33
+
34
+ if Dir['output/**/*'].select { |entry| File.file?(entry) }.empty?
35
+ warn 'vanagon: Error: No packages to ship in the "output" directory. Maybe build some first?'
36
+ exit 1
37
+ end
38
+
39
+ require 'packaging'
40
+ Pkg::Util::RakeUtils.load_packaging_tasks
41
+ Pkg::Util::RakeUtils.invoke_task('pl:jenkins:ship', 'artifacts', 'output')
42
+ begin
43
+ Pkg::Util::RakeUtils.invoke_task('pl:jenkins:ship_to_artifactory', 'output')
44
+ rescue LoadError
45
+ warn artifactory_warning
46
+ rescue StandardError
47
+ warn artifactory_warning
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,34 @@
1
+ require 'docopt'
2
+
3
+ class Vanagon
4
+ class CLI
5
+ class Sign < Vanagon::CLI
6
+ DOCUMENTATION = <<~DOCOPT.freeze
7
+ Usage:
8
+ sign [--help]
9
+
10
+ Options:
11
+ -h, --help Display help
12
+ DOCOPT
13
+
14
+ def parse(argv)
15
+ Docopt.docopt(DOCUMENTATION, { argv: argv })
16
+ rescue Docopt::Exit => e
17
+ puts e.message
18
+ exit 1
19
+ end
20
+
21
+ def run(_)
22
+ ENV['PROJECT_ROOT'] = Dir.pwd
23
+ if Dir['output/**/*'].select { |entry| File.file?(entry) }.empty?
24
+ warn 'sign: Error: No packages to sign in the "output" directory. Maybe build some first?'
25
+ exit 1
26
+ end
27
+
28
+ require 'packaging'
29
+ Pkg::Util::RakeUtils.load_packaging_tasks
30
+ Pkg::Util::RakeUtils.invoke_task('pl:jenkins:sign_all', 'output')
31
+ end
32
+ end
33
+ end
34
+ end
@@ -29,7 +29,6 @@ class Vanagon
29
29
  components = options[:components] || []
30
30
  only_build = options[:only_build]
31
31
  target = options[:target]
32
- engine = options[:engine] || 'pooler'
33
32
 
34
33
  @platform = Vanagon::Platform.load_platform(platform, File.join(@@configdir, "platforms"))
35
34
  @project = Vanagon::Project.load_project(project, File.join(@@configdir, "projects"), @platform, components)
@@ -40,9 +39,14 @@ class Vanagon
40
39
 
41
40
  @remote_workdir = options[:"remote-workdir"]
42
41
 
43
- load_engine(engine, @platform, target)
44
- rescue LoadError => e
45
- raise Vanagon::Error.wrap(e, "Could not load the desired engine '#{engine}'")
42
+ if options[:engine]
43
+ # Use the explicitly configured engine.
44
+ load_engine_object(options[:engine], @platform, target)
45
+ else
46
+ # Use 'pooler' as a default, but also apply selection logic that may
47
+ # choose something different based on platform configuration.
48
+ load_engine('pooler', @platform, target)
49
+ end
46
50
  end
47
51
 
48
52
  def filter_out_components(only_build)
@@ -73,8 +77,8 @@ class Vanagon
73
77
  def load_engine_object(engine_type, platform, target)
74
78
  require "vanagon/engine/#{engine_type}"
75
79
  @engine = Object::const_get("Vanagon::Engine::#{camelize(engine_type)}").new(platform, target, remote_workdir: remote_workdir)
76
- rescue StandardError
77
- fail "No such engine '#{camelize(engine_type)}'"
80
+ rescue StandardError, ScriptError => e
81
+ raise Vanagon::Error.wrap(e, "Could not load the desired engine '#{engine_type}'")
78
82
  end
79
83
 
80
84
  def camelize(string)
@@ -140,7 +144,7 @@ class Vanagon
140
144
  @project.make_bill_of_materials(workdir)
141
145
  # Don't generate packaging artifacts if no_packaging is set
142
146
  @project.generate_packaging_artifacts(workdir) unless @project.no_packaging
143
- @project.save_manifest_json(@platform)
147
+ @project.save_manifest_json(@platform, workdir)
144
148
  @engine.ship_workdir(workdir)
145
149
  @engine.dispatch("(cd #{@engine.remote_workdir}; #{@platform.make} #{make_target})")
146
150
  @engine.retrieve_built_artifact(@project.artifacts_to_fetch, @project.no_packaging)
@@ -1,5 +1,6 @@
1
- require 'vanagon/engine/base'
2
1
  require 'json'
2
+ require 'vanagon/engine/base'
3
+ require 'yaml'
3
4
 
4
5
  class Vanagon
5
6
  class Engine
@@ -82,9 +83,15 @@ class Vanagon
82
83
  # {"name":"aix-53-ppc","engine":"always_be_scheduling"}
83
84
  # ```
84
85
  class AlwaysBeScheduling < Base
86
+ attr_reader :token
87
+ attr_reader :token_vmpooler
88
+
85
89
  def initialize(platform, target, **opts)
86
90
  super
87
91
 
92
+ @available_abs_endpoint = "https://abs-prod.k8s.infracore.puppet.net/api/v2"
93
+ @token_vmpooler = ENV['VMPOOLER_TOKEN']
94
+ @token = load_token
88
95
  Vanagon::Driver.logger.debug "AlwaysBeScheduling engine invoked."
89
96
  end
90
97
 
@@ -94,6 +101,7 @@ class Vanagon
94
101
  end
95
102
 
96
103
  # return the platform name as the "host" name
104
+ # order of preference: abs_resource_name, vmpooler_template or name
97
105
  def build_host_name
98
106
  if @platform.abs_resource_name
99
107
  @platform.abs_resource_name
@@ -103,6 +111,268 @@ class Vanagon
103
111
  @platform.name
104
112
  end
105
113
  end
114
+
115
+ # Retrieve the ABS token from an environment variable
116
+ # ("ABS_TOKEN") or from a number of potential configuration
117
+ # files (~/.vanagon-token or ~/.vmfloaty.yml).
118
+ # @return [String, nil] token for use with the vmpooler
119
+ def load_token
120
+ ENV['ABS_TOKEN'] || token_from_file
121
+ end
122
+
123
+ # a wrapper method around retrieving a token,
124
+ # with an explicitly ordered preference for a Vanagon-specific
125
+ # token file or a preexisting vmfoaty yaml file.
126
+ #
127
+ # @return [String, nil] token for use with the vmpooler
128
+ def token_from_file
129
+ read_vanagon_token || read_vmfloaty_token
130
+ end
131
+ private :token_from_file
132
+
133
+ # Read an ABS/vmpooler token from the plaintext vanagon-token file,
134
+ # as outlined in the project README.
135
+ # The first line should be the ABS token
136
+ # and the second (optional) line should be the vmpooler token
137
+ # Saves the second line into the @token_vmpooler variable
138
+ #
139
+ # @return [String, nil] the vanagon vmpooler token value
140
+ def read_vanagon_token(path = "~/.vanagon-token")
141
+ absolute_path = File.expand_path(path)
142
+ return nil unless File.exist?(absolute_path)
143
+
144
+ warn "Reading ABS token from: #{path}"
145
+ contents = File.read(absolute_path).chomp
146
+ lines = contents.each_line.map(&:chomp)
147
+
148
+ abs = lines.shift
149
+ @token_vmpooler = lines.shift
150
+
151
+ warn "Please add a second line with the vmpooler token to be able to modify or see the VM in floaty/bit-bar" if @token_vmpooler.nil?
152
+ return abs
153
+ end
154
+ private :read_vanagon_token
155
+
156
+ # Read a vmpooler token from the yaml formatted vmfloaty config,
157
+ # as outlined by the vmfloaty project:
158
+ # https://github.com/puppetlabs/vmfloaty
159
+ #
160
+ # It returns the top-level token value or the first 'abs' service
161
+ # token value it finds in the services list
162
+ # it sets @token_vmpooler with the optional vmpooler token
163
+ # if the abs services has a vmpooler_fallback, and that service
164
+ # has a token eg
165
+ #
166
+ # TOP-LEVEL DEFINED => would get only the abs token
167
+ # user: 'jdoe'
168
+ # url: 'https://abs.example.net'
169
+ # token: '456def789'
170
+ #
171
+ # MULTIPLE SERVICES DEFINED => would get the abs token in 'abs-prod' and the vmpooler token in 'vmpooler-prod'
172
+ # user: 'jdoe'
173
+ # services:
174
+ # abs-prod:
175
+ # type: 'abs'
176
+ # url: 'https://abs.example.net/api/v2'
177
+ # token: '123abc456'
178
+ # vmpooler_fallback: 'vmpooler-prod'
179
+ # vmpooler-dev:
180
+ # type: 'vmpooler'
181
+ # url: 'https://vmpooler-dev.example.net'
182
+ # token: '987dsa654'
183
+ # vmpooler-prod:
184
+ # type: 'vmpooler'
185
+ # url: 'https://vmpooler.example.net'
186
+ # token: '456def789'
187
+ #
188
+ # @return [String, nil] the vmfloaty vmpooler token value
189
+ def read_vmfloaty_token(path = "~/.vmfloaty.yml") # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
190
+ absolute_path = File.expand_path(path)
191
+ return nil unless File.exist?(absolute_path)
192
+
193
+ yaml_config = YAML.load_file(absolute_path)
194
+ abs_token = nil
195
+ abs_service_name = nil
196
+ if yaml_config['token'] # top level
197
+ abs_token = yaml_config['token']
198
+ elsif yaml_config['services']
199
+ yaml_config['services'].each do |name, value|
200
+ if value['type'] == "abs" && value['token']
201
+ abs_token = value['token']
202
+ abs_service_name = name
203
+ vmpooler_fallback = value['vmpooler_fallback']
204
+ unless vmpooler_fallback.nil? || yaml_config['services'][vmpooler_fallback].nil? || yaml_config['services'][vmpooler_fallback]['token'].nil?
205
+ @token_vmpooler = yaml_config['services'][vmpooler_fallback]['token']
206
+ end
207
+ break
208
+ end
209
+ end
210
+ end
211
+ message = "Reading ABS token from: #{path}"
212
+ if abs_service_name
213
+ message.concat(" for service named #{abs_service_name}")
214
+ if @token_vmpooler.nil?
215
+ message.concat(" but there was no vmpooler_fallback value, please add one if you want to be able to modify the VM via bit-bar/floaty")
216
+ else
217
+ message.concat(" with a vmpooler_fallback token")
218
+ end
219
+ end
220
+ warn message
221
+ return abs_token
222
+ end
223
+ private :read_vmfloaty_token
224
+
225
+ # This method is used to obtain a vm to build upon using Puppet's internal
226
+ # ABS (https://github.com/puppetlabs/always-be-scheduling) which is a level of abstraction for other
227
+ # engines using similar APIs
228
+ # @raise [Vanagon::Error] if a target cannot be obtained
229
+ def select_target
230
+ @pooler = select_target_from(@available_abs_endpoint)
231
+ raise Vanagon::Error, "Something went wrong getting a target vm to build on" if @pooler.empty?
232
+ end
233
+
234
+ # Attempt to provision a host from a specific pooler.
235
+ def select_target_from(pooler) # rubocop:disable Metrics/AbcSize
236
+ request_object = build_request_object
237
+
238
+ warn "Requesting VMs with job_id: #{@saved_job_id}. Will poll for up to an hour."
239
+ #the initial request is always replied with "come back again"
240
+ response = Vanagon::Utilities.http_request_generic(
241
+ "#{pooler}/request",
242
+ 'POST',
243
+ request_object.to_json,
244
+ { 'X-AUTH-TOKEN' => @token }
245
+ )
246
+
247
+ unless response.code == "202"
248
+ warn "failed to request ABS with code #{response.code}"
249
+ if valid_json?(response.body)
250
+ response_json = JSON.parse(response.body)
251
+ warn "reason: #{response_json['reason']}"
252
+ end
253
+ return ''
254
+ end
255
+ response_body = check_queue(pooler, request_object)
256
+
257
+ return '' unless response_body["ok"]
258
+ @target = response_body[build_host_name]['hostname']
259
+ Vanagon::Driver.logger.info "Reserving #{@target} (#{build_host_name}) [#{@token ? 'token used' : 'no token used'}]"
260
+ return pooler
261
+ end
262
+
263
+ # main loop where the status of the request is checked, to see if the request
264
+ # has been allocated
265
+ def check_queue(pooler, request_object)
266
+ retries = 360 # ~ one hour
267
+ response_body = nil
268
+ begin
269
+ (1..retries).each do |i|
270
+ response = Vanagon::Utilities.http_request_generic(
271
+ "#{pooler}/request",
272
+ 'POST',
273
+ request_object.to_json,
274
+ { 'X-AUTH-TOKEN' => @token }
275
+ )
276
+ response_body = validate_queue_status_response(response.code, response.body)
277
+ break if response_body
278
+
279
+ sleep_seconds = 10 if i >= 10
280
+ sleep_seconds = i if i < 10
281
+ warn "Waiting #{sleep_seconds} seconds to check if ABS request has been filled. (x#{i})"
282
+
283
+ sleep(sleep_seconds)
284
+ end
285
+ rescue SystemExit, Interrupt
286
+ warn "\nVanagon interrupted during mains ABS polling. Make sure you delete the requested job_id #{@saved_job_id}"
287
+ raise
288
+ end
289
+ response_body = translated(response_body, @saved_job_id)
290
+ response_body
291
+ end
292
+
293
+ def validate_queue_status_response(status_code, body)
294
+ case status_code
295
+ when "200"
296
+ return JSON.parse(body) unless body.empty? || !valid_json?(body)
297
+ when "202"
298
+ return nil
299
+ when "401"
300
+ raise Vanagon::Error, "HTTP #{status_code}: The token provided could not authenticate.\n#{body}"
301
+ when "503"
302
+ return nil
303
+ else
304
+ raise Vanagon::Error, "HTTP #{status_code}: request to ABS failed!\n#{body}"
305
+ end
306
+ end
307
+
308
+ # This method is used to tell the ABS to delete the job_id requested
309
+ # otherwise the resources will eventually get allocated asynchronously
310
+ # and will keep running until the end of their lifetime.
311
+ def teardown # rubocop:disable Metrics/AbcSize
312
+ request_object = {
313
+ 'job_id' => @saved_job_id,
314
+ }
315
+
316
+ response = Vanagon::Utilities.http_request_generic(
317
+ "#{@available_abs_endpoint}/return",
318
+ "POST",
319
+ request_object.to_json,
320
+ { 'X-AUTH-TOKEN' => @token }
321
+ )
322
+ if response && response.body == 'OK'
323
+ Vanagon::Driver.logger.info "#{@saved_job_id} has been scheduled for removal"
324
+ warn "#{@saved_job_id} has been scheduled for removal"
325
+ else
326
+ Vanagon::Driver.logger.info "#{@saved_job_id} could not be scheduled for removal: #{response.body}"
327
+ warn "#{@saved_job_id} could not be scheduled for removal"
328
+ end
329
+ rescue Vanagon::Error => e
330
+ Vanagon::Driver.logger.info "#{@saved_job_id} could not be scheduled for removal (#{e.message})"
331
+ warn "#{@saved_job_id} could not be scheduled for removal (#{e.message})"
332
+ end
333
+
334
+ private
335
+
336
+ def translated(response_body, job_id)
337
+ vmpooler_formatted_body = { 'job_id' => job_id }
338
+
339
+ response_body.each do |host| # in this context there should be only one host
340
+ vmpooler_formatted_body[host['type']] = { 'hostname' => host['hostname'] }
341
+ end
342
+ vmpooler_formatted_body['ok'] = true
343
+
344
+ vmpooler_formatted_body
345
+ end
346
+
347
+ def build_request_object
348
+ user = ENV['USER'] || ENV['USERNAME'] || 'unknown'
349
+
350
+ @saved_job_id = user + "-" + DateTime.now.strftime('%Q')
351
+ request_object = {
352
+ :resources => { build_host_name => 1 },
353
+ :job => {
354
+ :id => @saved_job_id,
355
+ :tags => {
356
+ :jenkins_build_url => ENV['BUILD_URL'],
357
+ :project => ENV['JOB_NAME'] || 'vanagon',
358
+ :created_by => user,
359
+ :user => user
360
+ },
361
+ },
362
+ :priority => 1, # DO NOT use priority 1 in automated CI runs
363
+ }
364
+ unless @token_vmpooler.nil?
365
+ request_object[:vm_token] = @token_vmpooler
366
+ end
367
+ request_object
368
+ end
369
+
370
+ def valid_json?(json)
371
+ JSON.parse(json)
372
+ return true
373
+ rescue TypeError, JSON::ParserError
374
+ return false
375
+ end
106
376
  end
107
377
  end
108
378
  end