vagrant-pe_build 0.8.8 → 0.9.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.
- checksums.yaml +4 -4
- data/.gitignore +8 -0
- data/.rspec +2 -0
- data/.yardopts +1 -0
- data/CHANGELOG +12 -0
- data/Gemfile +18 -9
- data/README.markdown +2 -0
- data/Rakefile +5 -0
- data/acceptance/pe_build/pe_build_spec.rb +50 -0
- data/acceptance/skeletons/pe_build/Vagrantfile +24 -0
- data/acceptance/skeletons/pe_build/agent-3.x.txt.erb +17 -0
- data/lib/pe_build/archive.rb +11 -7
- data/lib/pe_build/config/global.rb +33 -2
- data/lib/pe_build/config/pe_bootstrap.rb +4 -24
- data/lib/pe_build/config_builder/global.rb +16 -0
- data/lib/pe_build/config_builder/pe_bootstrap.rb +2 -0
- data/lib/pe_build/provisioner/pe_bootstrap.rb +15 -0
- data/lib/pe_build/transfer.rb +23 -4
- data/lib/pe_build/transfer/file.rb +16 -10
- data/lib/pe_build/transfer/open_uri.rb +25 -22
- data/lib/pe_build/util/versioned_path.rb +33 -0
- data/lib/pe_build/version.rb +1 -1
- data/spec/shared/helpers/webserver_context.rb +30 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/unit/config/global_spec.rb +82 -0
- data/spec/unit/config/pe_bootstrap_spec.rb +22 -0
- data/spec/unit/provisioner/pe_bootstrap_spec.rb +47 -0
- data/tasks/acceptance.rake +25 -0
- data/tasks/spec.rake +3 -0
- data/templates/locales/en.yml +4 -3
- data/vagrant-pe_build.gemspec +7 -3
- data/vagrant-spec.config.example.rb +19 -0
- metadata +46 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 269bf2fbb29c95381a0977397d40d4035c4a51e0
|
4
|
+
data.tar.gz: e50cff262ba84a0782fe50dedf575a3afc783574
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b25346e8a16ddd1005c5e6bcec8e86a585b255227fff9b5266a732013d9d7dddee4a9c229867010af66fb7b1868b53bbea2236eab4522e5b8a814b5d23ed0809
|
7
|
+
data.tar.gz: 8064eaebceb1ec8e12d11bf9c9d4ad5e22e0cc1b3b4db9524f2d390e9953abf5013791042e440c104940e05c1e5629d1ea5b5f6c2aca0f3f90d15509ddfbe867
|
data/.gitignore
CHANGED
data/.rspec
ADDED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--markup markdown
|
data/CHANGELOG
CHANGED
@@ -1,6 +1,18 @@
|
|
1
1
|
vagrant-pe_build
|
2
2
|
================
|
3
3
|
|
4
|
+
0.9.0
|
5
|
+
-----
|
6
|
+
|
7
|
+
2014-07-09
|
8
|
+
|
9
|
+
This is a backwards compatible feature release.
|
10
|
+
|
11
|
+
* Add two new config attributes, `version_file` and `series`, that add
|
12
|
+
flexibility to the installation location.
|
13
|
+
* Internal code cleanup and re-organization.
|
14
|
+
* Add unit and acceptance tests.
|
15
|
+
|
4
16
|
0.8.8
|
5
17
|
-----
|
6
18
|
|
data/Gemfile
CHANGED
@@ -1,19 +1,28 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
|
+
ruby '2.0.0' # Required by Vagrant 1.4 and newer.
|
2
3
|
|
3
|
-
|
4
|
+
ENV['TEST_VAGRANT_VERSION'] ||= 'v1.6.3'
|
5
|
+
|
6
|
+
# Wrapping gemspec in the :plugins group causes Vagrant 1.5 and newer to
|
7
|
+
# automagically load this plugin during acceptance tests.
|
8
|
+
group :plugins do
|
9
|
+
gemspec
|
10
|
+
end
|
4
11
|
|
5
12
|
group :doc do
|
6
13
|
gem 'yard', '~> 0.8.7'
|
7
14
|
gem 'redcarpet'
|
8
15
|
end
|
9
16
|
|
10
|
-
group :
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
end
|
17
|
+
group :test do
|
18
|
+
if ENV['TEST_VAGRANT_VERSION'] == 'HEAD'
|
19
|
+
gem 'vagrant', :github => 'mitchellh/vagrant', :branch => 'master'
|
20
|
+
else
|
21
|
+
gem 'vagrant', :github => 'mitchellh/vagrant', :tag => ENV['TEST_VAGRANT_VERSION']
|
22
|
+
end
|
16
23
|
|
17
|
-
|
18
|
-
|
24
|
+
# Pinned on 05/05/2014. Compatible with Vagrant 1.5.x and 1.6.x.
|
25
|
+
gem 'vagrant-spec', :github => 'mitchellh/vagrant-spec', :ref => 'aae28ee'
|
19
26
|
end
|
27
|
+
|
28
|
+
eval_gemfile "#{__FILE__}.local" if File.exists? "#{__FILE__}.local"
|
data/README.markdown
CHANGED
@@ -177,6 +177,8 @@ Requirements
|
|
177
177
|
|
178
178
|
[vagranthosts]: https://github.com/adrienthebo/vagrant-hosts
|
179
179
|
|
180
|
+
Ensure VMs have a FQDN set before installing PE. The easiest way to do this is by setting the `hostname` attribute of the VM configuration.
|
181
|
+
|
180
182
|
Puppet Enterprise relies on SSL for security so you'll need to ensure that your
|
181
183
|
SSL configuration isn't borked. [vagrant-hosts][vagranthosts] is recommended to
|
182
184
|
configure VMs with semi-sane DNS.
|
data/Rakefile
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
shared_examples 'provider/provisioner/pe_build' do |provider, options|
|
2
|
+
if !File.file?(options[:box])
|
3
|
+
raise ArgumentError,
|
4
|
+
"A box file must be downloaded for provider: #{provider}. Try: rake acceptance:setup"
|
5
|
+
end
|
6
|
+
|
7
|
+
include_context 'acceptance'
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
environment.skeleton('pe_build')
|
11
|
+
assert_execute('vagrant', 'box', 'add', 'box', options[:box])
|
12
|
+
end
|
13
|
+
|
14
|
+
after(:each) do
|
15
|
+
# Ensure any VMs that survived tests are cleaned up.
|
16
|
+
assert_execute('vagrant', 'destroy', '--force', log: false)
|
17
|
+
end
|
18
|
+
|
19
|
+
context 'when download_root is set to a local directory' do
|
20
|
+
let(:extra_env) do
|
21
|
+
vars = options[:env_vars].dup
|
22
|
+
vars['PE_BUILD_DOWNLOAD_ROOT'] = options[:archive_path]
|
23
|
+
|
24
|
+
vars
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'provisions with pe_build' do
|
28
|
+
assert_execute('vagrant', 'up', "--provider=#{provider}", 'explicit-version')
|
29
|
+
assert_execute('vagrant', 'up', "--provider=#{provider}", 'latest-version')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'when download_root is set to a webserver' do
|
34
|
+
let(:webserver_port) { 3838 }
|
35
|
+
let(:webserver_path) { options[:archive_path] }
|
36
|
+
include_context 'webserver'
|
37
|
+
|
38
|
+
let(:extra_env) do
|
39
|
+
vars = options[:env_vars].dup
|
40
|
+
vars['PE_BUILD_DOWNLOAD_ROOT'] = "http://localhost:#{webserver_port}/"
|
41
|
+
|
42
|
+
vars
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'provisions with pe_build' do
|
46
|
+
assert_execute('vagrant', 'up', "--provider=#{provider}", 'explicit-version')
|
47
|
+
assert_execute('vagrant', 'up', "--provider=#{provider}", 'latest-version')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
Vagrant.configure('2') do |config|
|
2
|
+
config.pe_build.download_root = ENV['PE_BUILD_DOWNLOAD_ROOT']
|
3
|
+
config.vm.box = 'box'
|
4
|
+
|
5
|
+
config.vm.define 'explicit-version' do |node|
|
6
|
+
node.vm.provision :pe_bootstrap do |p|
|
7
|
+
p.version = '3.2.3'
|
8
|
+
p.role = :agent
|
9
|
+
# Basically the stock answer file with:
|
10
|
+
# q_fail_on_unsuccessful_master_lookup=n
|
11
|
+
p.answer_file = File.join(File.dirname(__FILE__), 'agent-3.x.txt.erb')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
config.vm.define 'latest-version' do |node|
|
16
|
+
node.vm.provision :pe_bootstrap do |p|
|
17
|
+
p.version_file = 'LATEST'
|
18
|
+
p.role = :agent
|
19
|
+
# Basically the stock answer file with:
|
20
|
+
# q_fail_on_unsuccessful_master_lookup=n
|
21
|
+
p.answer_file = File.join(File.dirname(__FILE__), 'agent-3.x.txt.erb')
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
q_fail_on_unsuccessful_master_lookup=y
|
2
|
+
q_install=y
|
3
|
+
q_puppet_cloud_install=n
|
4
|
+
q_puppet_enterpriseconsole_install=n
|
5
|
+
q_puppet_symlinks_install=y
|
6
|
+
q_puppetagent_certname=<%= machine_hostname %>
|
7
|
+
q_puppetagent_install=y
|
8
|
+
q_puppetagent_server=<%= @config.master %>
|
9
|
+
q_puppetca_install=n
|
10
|
+
q_puppetdb_hostname=
|
11
|
+
q_puppetdb_install=n
|
12
|
+
q_puppetdb_port=
|
13
|
+
q_puppetmaster_install=n
|
14
|
+
q_vendor_packages_install=y
|
15
|
+
q_continue_or_reenter_master_hostname=c
|
16
|
+
q_verify_packages=y
|
17
|
+
q_fail_on_unsuccessful_master_lookup=n
|
data/lib/pe_build/archive.rb
CHANGED
@@ -27,6 +27,11 @@ module PEBuild
|
|
27
27
|
# @return [String] The version of Puppet Enterprise
|
28
28
|
attr_accessor :version
|
29
29
|
|
30
|
+
# (see PEBuild::Config::Global#series)
|
31
|
+
#
|
32
|
+
# @see PEBuild::Config::Global#series
|
33
|
+
attr_accessor :series
|
34
|
+
|
30
35
|
# @!attribute [rw] filename
|
31
36
|
# @return [String] The filename. Thing
|
32
37
|
attr_accessor :filename
|
@@ -62,8 +67,7 @@ module PEBuild
|
|
62
67
|
uri = URI.parse(versioned_path("#{str}/#{@filename}"))
|
63
68
|
dst = File.join(@archive_dir, versioned_path(@filename))
|
64
69
|
|
65
|
-
|
66
|
-
transfer.copy
|
70
|
+
PEBuild::Transfer.copy(uri, dst)
|
67
71
|
end
|
68
72
|
|
69
73
|
# @param fs_dir [String] The base directory to extract the installer to
|
@@ -103,11 +107,11 @@ module PEBuild
|
|
103
107
|
end
|
104
108
|
|
105
109
|
def versioned_path(path)
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
110
|
+
result = path.dup
|
111
|
+
result.gsub!(/:version/, @version) if @version
|
112
|
+
result.gsub!(/:series/, @series) if @series
|
113
|
+
|
114
|
+
result
|
111
115
|
end
|
112
116
|
end
|
113
117
|
end
|
@@ -6,20 +6,49 @@ require 'uri'
|
|
6
6
|
class PEBuild::Config::Global < Vagrant.plugin('2', :config)
|
7
7
|
|
8
8
|
# @!attribute download_root
|
9
|
+
# @return [String] The root URI from which to download packages. The URI
|
10
|
+
# scheme must be one of the values listed in {PEBuild::Transfer::IMPLEMENTATIONS}.
|
11
|
+
# @since 0.1.0
|
9
12
|
attr_accessor :download_root
|
10
13
|
|
11
14
|
# @!attribute version
|
15
|
+
# @return [String] The version of PE to install. Must conform to
|
16
|
+
# `x.y.x[-optional-arbitrary-stuff]`. Used to determine the name of the
|
17
|
+
# PE installer archive if {#filename} is unset.
|
18
|
+
# @since 0.1.0
|
12
19
|
attr_accessor :version
|
13
20
|
|
21
|
+
# @!attribute version_file
|
22
|
+
# @return [String] The path to a file relative to {#download_root}. The
|
23
|
+
# contents of this file will be read and used to specify {#version}.
|
24
|
+
# @since 0.9.0
|
25
|
+
attr_accessor :version_file
|
26
|
+
|
27
|
+
# @!attribute series
|
28
|
+
# @return [String] The release series of PE. Completely optional and
|
29
|
+
# currently has no effect other than being an interpolation token
|
30
|
+
# available for use in {#download_root}.
|
31
|
+
#
|
32
|
+
# @since 0.9.0
|
33
|
+
attr_accessor :series
|
34
|
+
|
14
35
|
# @!attribute suffix
|
36
|
+
# @return [String] The distribution specifix suffix of the Puppet
|
37
|
+
# Enterprise installer to use.
|
38
|
+
# @since 0.1.0
|
15
39
|
attr_accessor :suffix
|
16
40
|
|
17
41
|
# @!attribute filename
|
42
|
+
# @return [String] The exact name of the PE installer archive. If missing,
|
43
|
+
# a name will be constructed from {#version}.
|
44
|
+
# @since 0.1.0
|
18
45
|
attr_accessor :filename
|
19
46
|
|
20
47
|
def initialize
|
21
48
|
@download_root = UNSET_VALUE
|
22
49
|
@version = UNSET_VALUE
|
50
|
+
@version_file = UNSET_VALUE
|
51
|
+
@series = UNSET_VALUE
|
23
52
|
@suffix = UNSET_VALUE
|
24
53
|
@filename = UNSET_VALUE
|
25
54
|
end
|
@@ -27,12 +56,14 @@ class PEBuild::Config::Global < Vagrant.plugin('2', :config)
|
|
27
56
|
include PEBuild::ConfigDefault
|
28
57
|
|
29
58
|
def finalize!
|
59
|
+
set_default :@version, nil
|
60
|
+
set_default :@version_file, nil
|
61
|
+
set_default :@series, nil
|
30
62
|
set_default :@suffix, :detect
|
31
63
|
set_default :@download_root, nil
|
32
64
|
set_default :@filename, nil
|
33
65
|
end
|
34
66
|
|
35
|
-
# @todo Convert error strings to I18n
|
36
67
|
def validate(machine)
|
37
68
|
errors = []
|
38
69
|
|
@@ -61,7 +92,7 @@ class PEBuild::Config::Global < Vagrant.plugin('2', :config)
|
|
61
92
|
if !(@version.match PE_VERSION_REGEX)
|
62
93
|
errors << errmsg
|
63
94
|
end
|
64
|
-
elsif @version !=
|
95
|
+
elsif @version != nil
|
65
96
|
errors << errmsg
|
66
97
|
end
|
67
98
|
end
|
@@ -68,6 +68,10 @@ class PEBuild::Config::PEBootstrap < PEBuild::Config::Global
|
|
68
68
|
# the provisioner will handle that.
|
69
69
|
def finalize!
|
70
70
|
|
71
|
+
# NOTE: The version default is copied from Config::Global. Can't call
|
72
|
+
# `super` here as it does weird things to `download_root`.
|
73
|
+
set_default :@version, nil
|
74
|
+
|
71
75
|
set_default :@role, :agent
|
72
76
|
set_default :@verbose, true
|
73
77
|
set_default :@master, 'master'
|
@@ -83,7 +87,6 @@ class PEBuild::Config::PEBootstrap < PEBuild::Config::Global
|
|
83
87
|
# We also need to run this after a default was set, otherwise we'll try to
|
84
88
|
# normalize UNSET_VALUE
|
85
89
|
@role = @role.intern
|
86
|
-
|
87
90
|
end
|
88
91
|
|
89
92
|
# @param machine [Vagrant::Machine]
|
@@ -105,12 +108,6 @@ class PEBuild::Config::PEBootstrap < PEBuild::Config::Global
|
|
105
108
|
|
106
109
|
private
|
107
110
|
|
108
|
-
def validate_version(errors, machine)
|
109
|
-
if @version == UNSET_VALUE and global_config_from(machine).pe_build.version == UNSET_VALUE
|
110
|
-
errors << I18n.t('pebuild.config.pe_bootstrap.errors.unset_version')
|
111
|
-
end
|
112
|
-
end
|
113
|
-
|
114
111
|
def validate_role(errors, machine)
|
115
112
|
unless VALID_ROLES.any? {|sym| @role == sym.intern}
|
116
113
|
errors << I18n.t(
|
@@ -164,21 +161,4 @@ class PEBuild::Config::PEBootstrap < PEBuild::Config::Global
|
|
164
161
|
)
|
165
162
|
end
|
166
163
|
end
|
167
|
-
|
168
|
-
# Safely access the global config
|
169
|
-
def global_config_from(machine)
|
170
|
-
case Vagrant::VERSION
|
171
|
-
when /^1\.[1234]/
|
172
|
-
# If we try to access the global config object directly from a validating
|
173
|
-
# machine, horrible things happen. To avoid this we access the environment's
|
174
|
-
# global config which should already be finalized.
|
175
|
-
env = machine.env.config_global
|
176
|
-
else # Vagrant 1.5.x and above
|
177
|
-
# This kinda seemed like the most direct replacement for config_global,
|
178
|
-
# but turned out not to really work. Returned a dummy object of some
|
179
|
-
# kind.
|
180
|
-
#env = vagrantfile.config
|
181
|
-
env = machine.config
|
182
|
-
end
|
183
|
-
end
|
184
164
|
end
|
@@ -6,6 +6,20 @@ class PEBuild::ConfigBuilder::Global < ::ConfigBuilder::Model::Base
|
|
6
6
|
# @return [String] The version of Puppet Enterprise to install.
|
7
7
|
def_model_attribute :version
|
8
8
|
|
9
|
+
# @!attribute [rw] version_file
|
10
|
+
# @return [String] The path to a file relative to {#download_root}. The
|
11
|
+
# contents of this file will be read and used to specify {#version}.
|
12
|
+
# @since 0.9.0
|
13
|
+
def_model_attribute :version_file
|
14
|
+
|
15
|
+
# @!attribute [rw] series
|
16
|
+
# @return [String] The release series of PE. Completely optional and
|
17
|
+
# currently has no effect other than being an interpolation token
|
18
|
+
# available for use in {#download_root}.
|
19
|
+
#
|
20
|
+
# @since 0.9.0
|
21
|
+
def_model_attribute :series
|
22
|
+
|
9
23
|
# @!attribute [rw] suffix
|
10
24
|
# @return [String] The distribution specifix suffix of the Puppet
|
11
25
|
# Enterprise installer to use.
|
@@ -24,6 +38,8 @@ class PEBuild::ConfigBuilder::Global < ::ConfigBuilder::Model::Base
|
|
24
38
|
Proc.new do |global_config|
|
25
39
|
global_config.pe_build.download_root = attr(:download_root) if attr(:download_root)
|
26
40
|
global_config.pe_build.version = attr(:version) if attr(:version)
|
41
|
+
global_config.pe_build.version_file = attr(:version_file) if attr(:version_file)
|
42
|
+
global_config.pe_build.series = attr(:series) if attr(:series)
|
27
43
|
global_config.pe_build.suffix = attr(:suffix) if attr(:suffix)
|
28
44
|
global_config.pe_build.filename = attr(:filename) if attr(:filename)
|
29
45
|
end
|
@@ -46,6 +46,8 @@ class PEBuild::ConfigBuilder::PEBootstrap < ::PEBuild::ConfigBuilder::Global
|
|
46
46
|
# Globally settable attributes
|
47
47
|
pe.download_root = attr(:download_root) if attr(:download_root)
|
48
48
|
pe.version = attr(:version) if attr(:version)
|
49
|
+
pe.version_file = attr(:version_file) if attr(:version_file)
|
50
|
+
pe.series = attr(:series) if attr(:series)
|
49
51
|
pe.suffix = attr(:suffix) if attr(:suffix)
|
50
52
|
pe.filename = attr(:filename) if attr(:filename)
|
51
53
|
|
@@ -2,6 +2,7 @@ require 'vagrant'
|
|
2
2
|
|
3
3
|
require 'pe_build/archive'
|
4
4
|
require 'pe_build/util/config'
|
5
|
+
require 'pe_build/util/versioned_path'
|
5
6
|
|
6
7
|
require 'log4r'
|
7
8
|
require 'fileutils'
|
@@ -13,6 +14,10 @@ module PEBuild
|
|
13
14
|
require 'pe_build/provisioner/pe_bootstrap/answers_file'
|
14
15
|
require 'pe_build/provisioner/pe_bootstrap/post_install'
|
15
16
|
|
17
|
+
class UnsetVersionError < Vagrant::Errors::VagrantError
|
18
|
+
error_key(:unset_version, 'pebuild.provisioner.pe_bootstrap.errors')
|
19
|
+
end
|
20
|
+
|
16
21
|
# @!attribute [r] work_dir
|
17
22
|
# @return [String] The path to the machine pe_build working directory
|
18
23
|
|
@@ -81,6 +86,15 @@ module PEBuild
|
|
81
86
|
end
|
82
87
|
|
83
88
|
def load_archive
|
89
|
+
# If a version file is set, use its contents to specify the PE version.
|
90
|
+
unless @config.version_file.nil?
|
91
|
+
path = "#{@config.download_root}/#{@config.version_file}"
|
92
|
+
path = PEBuild::Util::VersionedPath.versioned_path(path, @config.version, @config.series)
|
93
|
+
@config.version = PEBuild::Transfer.read(URI.parse(path))
|
94
|
+
end
|
95
|
+
|
96
|
+
raise UnsetVersionError if @config.version.nil?
|
97
|
+
|
84
98
|
if @config.suffix == :detect and @config.filename.nil?
|
85
99
|
filename = @machine.guest.capability('detect_installer', @config.version)
|
86
100
|
else
|
@@ -88,6 +102,7 @@ module PEBuild
|
|
88
102
|
end
|
89
103
|
|
90
104
|
@archive = PEBuild::Archive.new(filename, @machine.env)
|
105
|
+
@archive.series = @config.series
|
91
106
|
@archive.version = @config.version
|
92
107
|
end
|
93
108
|
|
data/lib/pe_build/transfer.rb
CHANGED
@@ -16,11 +16,31 @@ module PEBuild
|
|
16
16
|
nil => PEBuild::Transfer::File, # Assume that URIs without a scheme are files
|
17
17
|
}
|
18
18
|
|
19
|
-
|
19
|
+
# @param src [URI] The local file path path to the file to copy
|
20
|
+
# @param dst [String] The path to destination of the copied file
|
21
|
+
def self.copy(src, dst)
|
20
22
|
scheme = src.scheme
|
21
23
|
|
22
|
-
if (
|
23
|
-
|
24
|
+
if (mod = IMPLEMENTATIONS[scheme])
|
25
|
+
mod.copy(src, dst)
|
26
|
+
else
|
27
|
+
raise UnhandledURIScheme, :scheme => scheme,
|
28
|
+
:supported => IMPLEMENTATIONS.keys
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Return the contents of a local or remote file.
|
33
|
+
#
|
34
|
+
# @param src [URI] The URI of the source file.
|
35
|
+
# @raises [UnhandledURIScheme] If the URI uses an unsupported scheme.
|
36
|
+
# @return [String] The contents of the source file.
|
37
|
+
#
|
38
|
+
# @since 0.9.0
|
39
|
+
def self.read(src)
|
40
|
+
scheme = src.scheme
|
41
|
+
|
42
|
+
if (mod = IMPLEMENTATIONS[scheme])
|
43
|
+
mod.read(src)
|
24
44
|
else
|
25
45
|
raise UnhandledURIScheme, :scheme => scheme,
|
26
46
|
:supported => IMPLEMENTATIONS.keys
|
@@ -28,4 +48,3 @@ module PEBuild
|
|
28
48
|
end
|
29
49
|
end
|
30
50
|
end
|
31
|
-
|
@@ -1,20 +1,26 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
require 'pe_build/idempotent'
|
3
3
|
|
4
|
-
|
4
|
+
# @todo These methods fail in a messy way if something goes wrong. They should
|
5
|
+
# be refactored to raise proper errors.
|
6
|
+
# @api private
|
7
|
+
module PEBuild::Transfer::File
|
8
|
+
extend PEBuild::Idempotent
|
5
9
|
|
6
10
|
# @param src [URI] The local file path path to the file to copy
|
7
11
|
# @param dst [String] The path to destination of the copied file
|
8
|
-
def
|
9
|
-
|
10
|
-
@dst = dst
|
11
|
-
|
12
|
-
@logger = Log4r::Logger.new('vagrant::pe_build::transfer::file')
|
12
|
+
def self.copy(src, dst)
|
13
|
+
idempotent(dst) { FileUtils.cp src.path, dst }
|
13
14
|
end
|
14
15
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
# @param src [URI] The local file path path to the file to read
|
17
|
+
# @return [String] The contents of the file with leading and trailing
|
18
|
+
# whitespace removed.
|
19
|
+
#
|
20
|
+
# @since 0.9.0
|
21
|
+
def self.read(src)
|
22
|
+
File.read(src.path).strip
|
19
23
|
end
|
24
|
+
|
25
|
+
# TODO: Raise an appropriate exception when files do not exist.
|
20
26
|
end
|
@@ -4,7 +4,11 @@ require 'pe_build/idempotent'
|
|
4
4
|
require 'open-uri'
|
5
5
|
require 'progressbar'
|
6
6
|
|
7
|
-
|
7
|
+
# @api private
|
8
|
+
module PEBuild::Transfer::OpenURI
|
9
|
+
extend PEBuild::Idempotent
|
10
|
+
|
11
|
+
HEADERS = {'User-Agent' => "Vagrant/PEBuild (v#{PEBuild::VERSION})"}
|
8
12
|
|
9
13
|
class DownloadFailed < Vagrant::Errors::VagrantError
|
10
14
|
error_key(:download_failed, 'pebuild.transfer.open_uri')
|
@@ -12,36 +16,37 @@ class PEBuild::Transfer::OpenURI
|
|
12
16
|
|
13
17
|
# @param uri [URI] The http(s) URI to the file to copy
|
14
18
|
# @param dst [String] The path to destination of the copied file
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
include PEBuild::Idempotent
|
22
|
-
|
23
|
-
def copy
|
24
|
-
idempotent(@dst) do
|
25
|
-
tmpfile = download_file
|
26
|
-
FileUtils.mv(tmpfile, @dst)
|
19
|
+
def self.copy(uri, dst)
|
20
|
+
idempotent(dst) do
|
21
|
+
tmpfile = download_file(uri)
|
22
|
+
FileUtils.mv(tmpfile, dst)
|
27
23
|
end
|
28
24
|
rescue StandardError => e
|
29
|
-
raise DownloadFailed, :uri =>
|
25
|
+
raise DownloadFailed, :uri => uri, :msg => e.message
|
30
26
|
end
|
31
27
|
|
32
|
-
|
33
|
-
|
34
|
-
|
28
|
+
# @param uri [URI] The http(s) URI to the file to copy
|
29
|
+
# @return [String] The contents of the file with leading and trailing
|
30
|
+
# whitespace removed.
|
31
|
+
#
|
32
|
+
# @since 0.9.0
|
33
|
+
def self.read(uri)
|
34
|
+
uri.read(HEADERS.merge({'Accept' => 'text/plain'})).strip
|
35
|
+
rescue StandardError => e
|
36
|
+
raise DownloadFailed, :uri => uri, :msg => e.message
|
37
|
+
end
|
35
38
|
|
36
39
|
# Open a open-uri file handle for the given URL
|
37
40
|
#
|
41
|
+
# @param uri [URI]
|
38
42
|
# @return [IO]
|
39
|
-
def download_file
|
43
|
+
def self.download_file(uri)
|
40
44
|
progress = nil
|
41
45
|
|
42
46
|
content_length_proc = lambda do |length|
|
43
47
|
if length and length > 0
|
44
|
-
|
48
|
+
STDERR.puts "Fetching: #{uri}"
|
49
|
+
progress = ProgressBar.new("Fetching file", length, STDERR)
|
45
50
|
progress.file_transfer_mode
|
46
51
|
end
|
47
52
|
end
|
@@ -55,8 +60,6 @@ class PEBuild::Transfer::OpenURI
|
|
55
60
|
:progress_proc => progress_proc,
|
56
61
|
})
|
57
62
|
|
58
|
-
|
59
|
-
|
60
|
-
@uri.open(options)
|
63
|
+
uri.open(options)
|
61
64
|
end
|
62
65
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module PEBuild
|
2
|
+
module Util
|
3
|
+
# @api private
|
4
|
+
#
|
5
|
+
# @since 0.9.0
|
6
|
+
module VersionedPath
|
7
|
+
|
8
|
+
# Substitute release information into a path.
|
9
|
+
#
|
10
|
+
# @param path [String] A path.
|
11
|
+
# @param version [String, nil] A string that will be substituted for any
|
12
|
+
# `:version` token in `path`.
|
13
|
+
# @param series [String, nil] A string that will be substituted for any
|
14
|
+
# `:series` token in `path`.
|
15
|
+
#
|
16
|
+
# @return [String]
|
17
|
+
def self.versioned_path(path, version = nil, series = nil)
|
18
|
+
result = path.dup
|
19
|
+
result.gsub!(/:version/, version) unless version.nil?
|
20
|
+
result.gsub!(/:series/, series) unless series.nil?
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
|
25
|
+
# FIXME: This code is basically lifted from:
|
26
|
+
#
|
27
|
+
# PEBuild::Archive#versioned_path
|
28
|
+
#
|
29
|
+
# These two uses need to be cleaned up and consolidated.
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/lib/pe_build/version.rb
CHANGED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'webrick'
|
2
|
+
|
3
|
+
# This context runs a WEBRick server that is accessible to tests.
|
4
|
+
# The `webserver_port` and `webserver_path` will need to be specified before
|
5
|
+
# this context is included:
|
6
|
+
#
|
7
|
+
# let(:webserver_port) { ... }
|
8
|
+
# let(:webserver_path) { ... }
|
9
|
+
# include 'webserver'
|
10
|
+
shared_context 'webserver' do
|
11
|
+
before(:each) do
|
12
|
+
mime_types = {
|
13
|
+
'gz' => 'application/gzip',
|
14
|
+
'zip' => 'application/zip',
|
15
|
+
'tar' => 'application/x-tar',
|
16
|
+
}
|
17
|
+
|
18
|
+
@server = WEBrick::HTTPServer.new(
|
19
|
+
AccessLog: [],
|
20
|
+
Port: webserver_port,
|
21
|
+
DocumentRoot: webserver_path,
|
22
|
+
MimeTypes: mime_types)
|
23
|
+
@thr = Thread.new { @server.start }
|
24
|
+
end
|
25
|
+
|
26
|
+
after(:each) do
|
27
|
+
@server.shutdown rescue nil
|
28
|
+
@thr.join rescue nil
|
29
|
+
end
|
30
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'pe_build/config'
|
4
|
+
|
5
|
+
describe PEBuild::Config::Global do
|
6
|
+
# The `machine` is a required argument to the validation routine, but
|
7
|
+
# currently is not used by the Global config validation checks.
|
8
|
+
let(:machine) { double('machine') }
|
9
|
+
|
10
|
+
context 'when finalized with default values' do
|
11
|
+
before(:each) { subject.finalize! }
|
12
|
+
|
13
|
+
it 'passes validation' do
|
14
|
+
errors = subject.validate(machine)
|
15
|
+
|
16
|
+
expect(errors).to include('PE build global config' => [])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe 'version' do
|
21
|
+
it 'may be of the form x.y.z' do
|
22
|
+
subject.version = '3.0.0'
|
23
|
+
|
24
|
+
subject.finalize!
|
25
|
+
errors = subject.validate(machine)
|
26
|
+
|
27
|
+
expect(errors).to include('PE build global config' => [])
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'may be of the form x.y.z[-other-arbitrary-stuff]' do
|
31
|
+
subject.version = '2.8.0-42-gsomesha'
|
32
|
+
|
33
|
+
subject.finalize!
|
34
|
+
errors = subject.validate(machine)
|
35
|
+
|
36
|
+
expect(errors).to include('PE build global config' => [])
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'may not be x.y' do
|
40
|
+
subject.version = '3.1'
|
41
|
+
|
42
|
+
subject.finalize!
|
43
|
+
errors = subject.validate(machine)
|
44
|
+
|
45
|
+
# Casting the array to a string and using a regex matcher gives a nice
|
46
|
+
# diff in the case of failure.
|
47
|
+
expect(errors['PE build global config'].to_s).to match(/String is malformed/)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe 'download_root' do
|
52
|
+
PEBuild::Transfer::IMPLEMENTATIONS.keys.compact.each do |scheme|
|
53
|
+
it "accepts #{scheme}://" do
|
54
|
+
subject.download_root = "#{scheme}://foo"
|
55
|
+
|
56
|
+
subject.finalize!
|
57
|
+
errors = subject.validate(machine)
|
58
|
+
|
59
|
+
expect(errors).to include('PE build global config' => [])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'accepts a raw path' do
|
64
|
+
subject.download_root = 'foo/bar'
|
65
|
+
|
66
|
+
subject.finalize!
|
67
|
+
errors = subject.validate(machine)
|
68
|
+
|
69
|
+
expect(errors).to include('PE build global config' => [])
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'rejects foo://' do
|
73
|
+
subject.download_root = 'foo://bar'
|
74
|
+
|
75
|
+
subject.finalize!
|
76
|
+
errors = subject.validate(machine)
|
77
|
+
|
78
|
+
expect(errors['PE build global config'].to_s).to match(/cannot be handled by any file transferrers/)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'pe_build/config'
|
4
|
+
|
5
|
+
describe PEBuild::Config::PEBootstrap do
|
6
|
+
let(:machine) { double('machine') }
|
7
|
+
|
8
|
+
context 'when finalized with default values' do
|
9
|
+
before(:each) { subject.finalize! }
|
10
|
+
|
11
|
+
it 'passes validation' do
|
12
|
+
errors = subject.validate(machine)
|
13
|
+
|
14
|
+
expect(errors).to include('PE Bootstrap' => [])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# TODO: Spec test the validation functions. Not critical right now since it
|
19
|
+
# is pretty much testing tests. But, having specs is a good way for people to
|
20
|
+
# see precisely _what_ is allowed.
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'pe_build/provisioner/pe_bootstrap'
|
4
|
+
|
5
|
+
describe PEBuild::Provisioner::PEBootstrap do
|
6
|
+
include_context 'vagrant-unit'
|
7
|
+
|
8
|
+
let(:test_env) do
|
9
|
+
test_env = isolated_environment
|
10
|
+
test_env.vagrantfile <<-EOF
|
11
|
+
Vagrant.configure('2') do |config|
|
12
|
+
config.vm.define :test
|
13
|
+
end
|
14
|
+
EOF
|
15
|
+
|
16
|
+
test_env
|
17
|
+
end
|
18
|
+
let(:env) { test_env.create_vagrant_env }
|
19
|
+
let(:machine) { env.machine(:test, :dummy) }
|
20
|
+
let(:bootstrap_config) { PEBuild::Config::PEBootstrap.new }
|
21
|
+
|
22
|
+
# Mock the communicator to prevent SSH commands for being executed.
|
23
|
+
let(:communicator) { double('communicator') }
|
24
|
+
# Mock the guest operating system.
|
25
|
+
let(:guest) { double('guest') }
|
26
|
+
|
27
|
+
before (:each) do
|
28
|
+
machine.stub(:guest => guest)
|
29
|
+
machine.stub(:communicator => communicator)
|
30
|
+
end
|
31
|
+
|
32
|
+
after(:each) { test_env.close }
|
33
|
+
|
34
|
+
subject(:provisioner) { described_class.new(machine, bootstrap_config) }
|
35
|
+
|
36
|
+
|
37
|
+
describe 'when configured' do
|
38
|
+
context 'and no version is set' do
|
39
|
+
it 'raises an error' do
|
40
|
+
pending 'This is now done in the `provision` method which is difficult to isolate for a test'
|
41
|
+
expect { subject.configure(machine.config) }.to raise_error(
|
42
|
+
PEBuild::Provisioner::PEBootstrap::UnsetVersionError,
|
43
|
+
/version must be set/ )
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
namespace :acceptance do
|
2
|
+
ARTIFACT_DIR = File.join('acceptance', 'artifacts')
|
3
|
+
TEST_BOXES = %w[
|
4
|
+
https://vagrantcloud.com/puppetlabs/centos-6.5-64-nocm/version/2/provider/virtualbox.box
|
5
|
+
]
|
6
|
+
|
7
|
+
directory ARTIFACT_DIR
|
8
|
+
TEST_BOXES.each do |box_url|
|
9
|
+
file File.join(ARTIFACT_DIR, File.basename(box_url)) => ARTIFACT_DIR do |path|
|
10
|
+
puts 'Downloading: ' + box_url
|
11
|
+
Kernel.system 'curl', '-L', '-o', path.to_s, box_url
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
desc 'downloads test boxes and other artifacts'
|
16
|
+
task :setup => TEST_BOXES.map {|box_url| File.join(ARTIFACT_DIR, File.basename(box_url))}
|
17
|
+
|
18
|
+
desc 'runs acceptance tests'
|
19
|
+
task :run => :setup do
|
20
|
+
command = 'vagrant-spec test'
|
21
|
+
puts command
|
22
|
+
puts
|
23
|
+
exec(command)
|
24
|
+
end
|
25
|
+
end
|
data/tasks/spec.rake
ADDED
data/templates/locales/en.yml
CHANGED
@@ -17,6 +17,10 @@ en:
|
|
17
17
|
pe_bootstrap:
|
18
18
|
post_install: |-
|
19
19
|
Applying post-install configuration to Puppet Enterprise.
|
20
|
+
errors:
|
21
|
+
unset_version: |-
|
22
|
+
The Puppet Enterprise version must be set either on the global pe_build config
|
23
|
+
object or specified on a per-provisioner basis, but both were unset.
|
20
24
|
transfer:
|
21
25
|
open_uri:
|
22
26
|
download_failed: |-
|
@@ -48,9 +52,6 @@ en:
|
|
48
52
|
information on the shell provisioner can be found on the Vagrant website at
|
49
53
|
http://docs.vagrantup.com/v2/provisioning/shell.html
|
50
54
|
errors:
|
51
|
-
unset_version: |-
|
52
|
-
The Puppet Enterprise version must be set either on the global pe_build config
|
53
|
-
object or specified on a per-provisioner basis, but both were unset.
|
54
55
|
unknown_role: |-
|
55
56
|
The specified role %{role} is unhandled, must be one of %{known_roles}.
|
56
57
|
invalid_autosign_role: |-
|
data/vagrant-pe_build.gemspec
CHANGED
@@ -13,11 +13,15 @@ Gem::Specification.new do |gem|
|
|
13
13
|
|
14
14
|
gem.summary = "Vagrant provisioner for installing Puppet Enterprise"
|
15
15
|
|
16
|
-
gem.add_dependency 'progressbar'
|
17
|
-
gem.add_dependency 'minitar'
|
18
|
-
|
19
16
|
gem.files = %x{git ls-files -z}.split("\0")
|
20
17
|
gem.require_path = 'lib'
|
21
18
|
|
22
19
|
gem.license = 'Apache 2.0'
|
20
|
+
|
21
|
+
gem.add_runtime_dependency 'progressbar'
|
22
|
+
gem.add_runtime_dependency 'minitar'
|
23
|
+
|
24
|
+
gem.add_development_dependency 'rake'
|
25
|
+
# Pin to 2.14.x for compatibility with vagrant-spec.
|
26
|
+
gem.add_development_dependency 'rspec', '~> 2.14.0'
|
23
27
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'vagrant-spec/acceptance'
|
3
|
+
|
4
|
+
require_relative 'spec/shared/helpers/webserver_context'
|
5
|
+
|
6
|
+
Vagrant::Spec::Acceptance.configure do |c|
|
7
|
+
acceptance_dir = Pathname.new File.expand_path('../acceptance', __FILE__)
|
8
|
+
|
9
|
+
c.component_paths = [acceptance_dir.to_s]
|
10
|
+
c.skeleton_paths = [(acceptance_dir + 'skeletons').to_s]
|
11
|
+
|
12
|
+
c.provider 'virtualbox',
|
13
|
+
box: (acceptance_dir + 'artifacts' + 'virtualbox.box').to_s,
|
14
|
+
# This folder should be filled with PE tarballs for CentOS.
|
15
|
+
archive_path: (acceptance_dir + 'artifacts' + 'pe_archives').to_s,
|
16
|
+
env_vars: {
|
17
|
+
'VBOX_USER_HOME' => '{{homedir}}',
|
18
|
+
}
|
19
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: vagrant-pe_build
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adrien Thebo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-
|
11
|
+
date: 2014-07-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: progressbar
|
@@ -38,6 +38,34 @@ dependencies:
|
|
38
38
|
- - '>='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 2.14.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ~>
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 2.14.0
|
41
69
|
description:
|
42
70
|
email: adrien@somethingsinistral.net
|
43
71
|
executables: []
|
@@ -45,10 +73,16 @@ extensions: []
|
|
45
73
|
extra_rdoc_files: []
|
46
74
|
files:
|
47
75
|
- .gitignore
|
76
|
+
- .rspec
|
77
|
+
- .yardopts
|
48
78
|
- CHANGELOG
|
49
79
|
- Gemfile
|
50
80
|
- LICENSE
|
51
81
|
- README.markdown
|
82
|
+
- Rakefile
|
83
|
+
- acceptance/pe_build/pe_build_spec.rb
|
84
|
+
- acceptance/skeletons/pe_build/Vagrantfile
|
85
|
+
- acceptance/skeletons/pe_build/agent-3.x.txt.erb
|
52
86
|
- doc/answers/README.markdown
|
53
87
|
- doc/answers/agent.txt
|
54
88
|
- doc/answers/master-1.1.txt
|
@@ -108,8 +142,16 @@ files:
|
|
108
142
|
- lib/pe_build/unpack/tar.rb
|
109
143
|
- lib/pe_build/unpack/tar_gz.rb
|
110
144
|
- lib/pe_build/util/config.rb
|
145
|
+
- lib/pe_build/util/versioned_path.rb
|
111
146
|
- lib/pe_build/version.rb
|
112
147
|
- lib/vagrant-pe_build.rb
|
148
|
+
- spec/shared/helpers/webserver_context.rb
|
149
|
+
- spec/spec_helper.rb
|
150
|
+
- spec/unit/config/global_spec.rb
|
151
|
+
- spec/unit/config/pe_bootstrap_spec.rb
|
152
|
+
- spec/unit/provisioner/pe_bootstrap_spec.rb
|
153
|
+
- tasks/acceptance.rake
|
154
|
+
- tasks/spec.rake
|
113
155
|
- templates/answers/agent-1.x.txt.erb
|
114
156
|
- templates/answers/agent-2.0.x.txt.erb
|
115
157
|
- templates/answers/agent-2.x.txt.erb
|
@@ -120,6 +162,7 @@ files:
|
|
120
162
|
- templates/answers/master-3.x.txt.erb
|
121
163
|
- templates/locales/en.yml
|
122
164
|
- vagrant-pe_build.gemspec
|
165
|
+
- vagrant-spec.config.example.rb
|
123
166
|
homepage: https://github.com/adrienthebo/vagrant-pe_build
|
124
167
|
licenses:
|
125
168
|
- Apache 2.0
|
@@ -145,3 +188,4 @@ signing_key:
|
|
145
188
|
specification_version: 4
|
146
189
|
summary: Vagrant provisioner for installing Puppet Enterprise
|
147
190
|
test_files: []
|
191
|
+
has_rdoc:
|