rzo 0.1.0 → 0.2.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.
@@ -0,0 +1,137 @@
1
+ require 'rzo/app/subcommand'
2
+ require 'pp'
3
+ require 'erb'
4
+
5
+ module Rzo
6
+ class App
7
+ ##
8
+ # Produce a `Vagrantfile` in the top level puppet control repo.
9
+ #
10
+ # Load all rizzo config files, then produce the Vagrantfile from an ERB
11
+ # template.
12
+ class Generate < Subcommand
13
+ attr_reader :config
14
+
15
+ # The main run method for the subcommand.
16
+ def run
17
+ exit_status = 0
18
+ load_config!
19
+ # Vagrantfile
20
+ erbfile = File.expand_path('../templates/Vagrantfile.erb', __FILE__)
21
+ content = vagrantfile_content(erbfile, config)
22
+ write_file(opts[:vagrantfile]) { |fd| fd.write(content) }
23
+ say "Wrote vagrant config to #{opts[:vagrantfile]}"
24
+ exit_status
25
+ end
26
+
27
+ ##
28
+ # Return a list of agent node definitions suitable for the Vagrantfile
29
+ # template.
30
+ #
31
+ # @param [Hash] config The configuration hash used to fill in the ERB
32
+ # template.
33
+ #
34
+ # @return [Array<Hash>] list of agent nodes to fill in the Vagrantfile
35
+ # template.
36
+ def vagrantfile_agents(config)
37
+ pm_settings = puppetmaster_settings(config)
38
+ agent_nodes = [*config['nodes']].reject do |n|
39
+ pm_settings['name'].include?(n['name'])
40
+ end
41
+
42
+ agent_nodes.each do |n|
43
+ n.deep_merge(config['defaults'])
44
+ log.debug "puppetagent #{n['name']} = \n" + n.pretty_inspect
45
+ end
46
+
47
+ agent_nodes
48
+ end
49
+
50
+ ##
51
+ # Return a list of puppetmaster node definitions suitable for the
52
+ # Vagrantfile template.
53
+ #
54
+ # @param [Hash] config The configuration hash used to fill in the ERB
55
+ # template.
56
+ #
57
+ # @return [Array<Hash>] list of puppet master nodes to fill in the
58
+ # Vagrantfile template.
59
+ #
60
+ # rubocop:disable Metrics/AbcSize
61
+ def vagrantfile_puppet_masters(config)
62
+ pm_settings = puppetmaster_settings(config)
63
+ pm_names = pm_settings['name']
64
+
65
+ nodes = [*config['nodes']].find_all { |n| pm_names.include?(n['name']) }
66
+ nodes.each do |n|
67
+ n.deep_merge(config['defaults'])
68
+ n.deep_merge(pm_settings)
69
+ n[:puppetmaster] = true
70
+ log.debug "puppetmaster #{n['name']} = \n" + n.pretty_inspect
71
+ end
72
+ end
73
+ # rubocop:enable Metrics/AbcSize
74
+
75
+ ##
76
+ # Return the proxy configuration exception list as a string, or nil if not set.
77
+ #
78
+ # @param [Hash] config The configuration hash used to fill in the ERB
79
+ # template.
80
+ #
81
+ # @return [String,nil] proxy exclusion list or nil if not specified.
82
+ def proxy_config(config)
83
+ # Proxy Setting
84
+ return nil unless config['config']
85
+ config['config']['no_proxy'] || DEFAULT_NO_PROXY
86
+ end
87
+
88
+ ##
89
+ # Return a timestamp to embed in the output Vagrantfile. This is a method
90
+ # so it may be stubbed out in the tests.
91
+ def timestamp
92
+ Time.now
93
+ end
94
+
95
+ ##
96
+ # Return a string which is the Vagrantfile content of a filled in
97
+ # Vagrantfile erb template. The configuration data parsed by load_config!
98
+ # is expected as input, along with the template to fill in.
99
+ #
100
+ # The base templates directory is relative to the directory containing
101
+ # this file.
102
+ #
103
+ # @param [String] template The fully qualified path to the ERB template.
104
+ #
105
+ # @param [Hash] config The configuration hash used to fill in the ERB
106
+ # template.
107
+ #
108
+ # @return [String] the content of the filled in template.
109
+ def vagrantfile_content(template, config)
110
+ renderer = ERB.new(File.read(template), 0, '-')
111
+
112
+ no_proxy = proxy_config(config)
113
+
114
+ # Agent nodes [Array<Hash>]
115
+ agent_nodes = vagrantfile_agents(config)
116
+ # Puppet Master nodes [Array<Hash>]
117
+ puppet_master_nodes = vagrantfile_puppet_masters(config)
118
+
119
+ # nodes is used by the Vagrantfile.erb template
120
+ nodes = [*puppet_master_nodes, *agent_nodes]
121
+ content = renderer.result(binding)
122
+ content
123
+ end
124
+
125
+ ##
126
+ # dump out the puppetmaster settings from the config.
127
+ def puppetmaster_settings(config)
128
+ log.debug "config['puppetmaster'] = \n" + \
129
+ config['puppetmaster'].pretty_inspect
130
+ config['puppetmaster']
131
+ end
132
+
133
+ # Constants used by the Vagrantfile.erb template.
134
+ DEFAULT_NO_PROXY = 'localhost,127.0.0.1'.freeze
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,172 @@
1
+ require 'rzo/logging'
2
+ require 'deep_merge'
3
+ module Rzo
4
+ class App
5
+ # The base class for subcommands
6
+ class Subcommand
7
+ include Logging
8
+ extend Logging
9
+ # The options hash injected from the application controller via the
10
+ # initialize method.
11
+ attr_reader :opts
12
+ # The Rizzo configuration after loading ~/.rizzo.json (--config).
13
+ # See #load_config!
14
+ attr_reader :config
15
+
16
+ ##
17
+ # Delegated method to mock with fixture data.
18
+ def self.load_rizzo_config(fpath)
19
+ config_file = Pathname.new(fpath).expand_path
20
+ raise ErrorAndExit, "Cannot read config file #{config_file}" unless config_file.readable?
21
+ config = JSON.parse(config_file.read)
22
+ log.debug "Loaded #{config_file}"
23
+ config
24
+ rescue JSON::ParserError => e
25
+ raise ErrorAndExit, "Could not parse rizzo config #{config_file} #{e.message}"
26
+ end
27
+
28
+ # Initialize a subcommand with options injected by the application
29
+ # controller.
30
+ #
31
+ # @param [Hash] opts the Options hash initialized by the Application
32
+ # controller.
33
+ def initialize(opts = {}, stdout = $stdout, stderr = $stderr)
34
+ @opts = opts
35
+ @stdout = stdout
36
+ @stderr = stderr
37
+ reset_logging!(opts)
38
+ end
39
+
40
+ ##
41
+ # Default run method. Override this method in a subcommand sub-class
42
+ #
43
+ # @return [Fixnum] the exit status of the subcommand.
44
+ def run
45
+ error "Implement the run method in subclass #{self.class}"
46
+ 1
47
+ end
48
+
49
+ private
50
+
51
+ ##
52
+ # Load rizzo configuration. Populate @config.
53
+ #
54
+ # Read rizzo configuration by looping through control repos and stopping
55
+ # at first match and merge on top of local, defaults (~/.rizzo.json)
56
+ def load_config!
57
+ config = load_rizzo_config(opts[:config])
58
+ validate_config(config)
59
+ repos = config['control_repos']
60
+ @config = load_repo_configs(config, repos)
61
+ debug "Merged configuration: \n#{JSON.pretty_generate(@config)}"
62
+ validate_forwarded_ports(@config)
63
+ validate_ip_addresses(@config)
64
+ @config
65
+ end
66
+
67
+ ##
68
+ # Given a list of repository paths, load .rizzo.json from the root of each
69
+ # repository and return the result merged onto config. The merging
70
+ # behavior is implemented by
71
+ # [deep_merge](http://www.rubydoc.info/gems/deep_merge/1.1.1)
72
+ #
73
+ # @param [Hash] config the starting config hash. Repo config maps will be
74
+ # merged on top of this starting map.
75
+ #
76
+ # @param [Array] repos the list of repositories to load .rizzo.json from.
77
+ #
78
+ # @return [Hash] the merged configuration hash.
79
+ def load_repo_configs(config = {}, repos = [])
80
+ repos.each_with_object(config.dup) do |repo, hsh|
81
+ fp = Pathname.new(repo).expand_path + '.rizzo.json'
82
+ if fp.readable?
83
+ hsh.deep_merge!(load_rizzo_config(fp))
84
+ else
85
+ log.debug "Skipped #{fp} (it is not readable)"
86
+ end
87
+ end
88
+ end
89
+
90
+ ##
91
+ # Basic validation of the configuration file content.
92
+ #
93
+ # @param [Hash] config the configuration map
94
+ def validate_config(config)
95
+ errors = []
96
+ errors.push 'control_repos key is not an Array' unless config['control_repos'].is_a?(Array)
97
+ errors.each { |l| log.error l }
98
+ raise ErrorAndExit, 'Errors found in config file. Cannot proceed.' unless errors.empty?
99
+ end
100
+
101
+ ##
102
+ # Check for duplicate forwarded host ports across all hosts and exit
103
+ # non-zero with an error message if found.
104
+ def validate_forwarded_ports(config)
105
+ host_ports = []
106
+ [*config['nodes']].each do |node|
107
+ [*node['forwarded_ports']].each do |hsh|
108
+ port = hsh['host'].to_i
109
+ raise_port_err(port, node['name']) if host_ports.include?(port)
110
+ host_ports.push(port)
111
+ end
112
+ end
113
+ log.debug "host_ports = #{host_ports}"
114
+ end
115
+
116
+ ##
117
+ # Check for duplicate forwarded host ports across all hosts and exit
118
+ # non-zero with an error message if found.
119
+ def validate_ip_addresses(config)
120
+ ips = []
121
+ [*config['nodes']].each do |node|
122
+ if ip = node['ip']
123
+ raise_ip_err(ip, node['name']) if ips.include?(ip)
124
+ ips.push(ip)
125
+ end
126
+ end
127
+ log.debug "ips = #{ips}"
128
+ end
129
+
130
+ ##
131
+ # Helper to raise a duplicate port error
132
+ def raise_port_err(port, node)
133
+ raise ErrorAndExit, "host port #{port} on node #{node} " \
134
+ 'is a duplicate. Ports must be unique. Check .rizzo.json ' \
135
+ 'files in each control repository for duplicate forwarded_ports entries.'
136
+ end
137
+
138
+ ##
139
+ # Helper to raise a duplicate port error
140
+ def raise_ip_err(ip, node)
141
+ raise ErrorAndExit, "host ip #{ip} on node #{node} " \
142
+ 'is a duplicate. IP addresses must be unique. Check .rizzo.json ' \
143
+ 'files in each control repository for duplicate ip entries'
144
+ end
145
+
146
+ ##
147
+ # Load the base configuration and return it as a hash. This is necessary
148
+ # to get access to the `'control_repos'` top level key, which is expected
149
+ # to be an Array of fully qualified paths to control repo base
150
+ # directories.
151
+ #
152
+ # @param [String] fpath The fully qualified path to the configuration file
153
+ # to load.
154
+ #
155
+ # @return [Hash] The configuration map
156
+ def load_rizzo_config(fpath)
157
+ self.class.load_rizzo_config(fpath)
158
+ end
159
+
160
+ ##
161
+ # Write a file by yielding a file descriptor to the passed block. In the
162
+ # case of opening a file, the FD will automatically be closed.
163
+ def write_file(filepath)
164
+ case filepath
165
+ when 'STDOUT' then yield @stdout
166
+ when 'STDERR' then yield @stderr
167
+ else File.open(filepath, 'w') { |fd| yield fd }
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,55 @@
1
+ # This file generated with Rizzo <%= Rzo.version %> on <%= timestamp %>
2
+ # https://github.com/ghoneycutt/rizzo
3
+ Vagrant.configure(2) do |config|
4
+ # use 'vagrant plugin install vagrant-proxyconf' to install
5
+ if Vagrant.has_plugin?('vagrant-proxyconf')
6
+ config.proxy.http = ENV['HTTP_PROXY'] if ENV['HTTP_PROXY']
7
+ config.proxy.https = ENV['HTTPS_PROXY'] if ENV['HTTPS_PROXY']
8
+ <%- if no_proxy -%>
9
+ config.proxy.no_proxy = <%= no_proxy.inspect %>
10
+ <%- end -%>
11
+ end
12
+ <%- nodes.each do |nc| -%>
13
+
14
+ config.vm.define <%= nc['name'].inspect %>, autostart: false do |cfg|
15
+ cfg.vm.box = <%= nc['box'].inspect %>
16
+ cfg.vm.box_url = <%= nc['box_url'].inspect %>
17
+ cfg.vm.box_download_checksum = <%= nc['box_download_checksum'].inspect %>
18
+ cfg.vm.box_download_checksum_type = <%= nc['box_download_checksum_type'].inspect %>
19
+ cfg.vm.provider :virtualbox do |vb|
20
+ vb.customize ['modifyvm', :id, '--memory', <%= nc['memory'].inspect %>]
21
+ end
22
+ cfg.vm.hostname = <%= nc['hostname'].inspect %>
23
+ cfg.vm.network 'private_network',
24
+ ip: <%= nc['ip'].inspect %>,
25
+ netmask: <%= nc['netmask'].inspect %>
26
+ <%- [*nc['forwarded_ports']].each do |forwarded_port| -%>
27
+ cfg.vm.network 'forwarded_port',
28
+ guest: <%= forwarded_port['guest'].inspect %>,
29
+ host: <%= forwarded_port['host'].inspect %>
30
+ <%- end -%>
31
+ <%- synced_folders = nc['synced_folders'] || {} -%>
32
+ <%- synced_folders.each_pair do |k, v| -%>
33
+ cfg.vm.synced_folder <%= v['local'].inspect %>, <%= k.inspect %>,
34
+ owner: <%= v['owner'].inspect %>, group: <%= v['group'].inspect %>
35
+ <%- end -%>
36
+ <%- if nc['bootstrap_repo_path'] -%>
37
+ config.vm.synced_folder <%= nc['bootstrap_repo_path'].inspect %>,
38
+ <%= nc['bootstrap_guest_path'].inspect %>,
39
+ owner: 'vagrant', group: 'root'
40
+ <%- if nc[:puppetmaster] -%>
41
+ config.vm.provision 'shell', inline: <%= "echo 'modulepath = #{nc['modulepath'].join(':')}' > #{nc['bootstrap_guest_path']}/environment.conf".inspect %>
42
+ <%- end -%>
43
+ config.vm.provision 'shell', inline: <%= "/bin/bash #{nc['bootstrap_guest_path']}/#{nc['bootstrap_script_path']} #{nc['bootstrap_script_args']}".inspect %>
44
+ <%- if nc['update_packages'] -%>
45
+ config.vm.provision 'shell', inline: <%= nc['update_packages_command'].inspect %>
46
+ <%- end -%>
47
+ <%- if nc['shutdown'] -%>
48
+ config.vm.provision 'shell', inline: <%= nc['shutdown_command'].inspect %>
49
+ <%- end -%>
50
+ <%- end -%>
51
+ end
52
+ <%- end -%>
53
+ end
54
+ # -*- mode: ruby -*-
55
+ # vim:ft=ruby
data/lib/rzo/app.rb ADDED
@@ -0,0 +1,92 @@
1
+ require 'rzo'
2
+ require 'rzo/logging'
3
+ require 'rzo/option_parsing'
4
+ require 'rzo/app/config'
5
+ require 'rzo/app/generate'
6
+ require 'json'
7
+
8
+ module Rzo
9
+ ##
10
+ # The application controller. An instance of this class models the
11
+ # application lifecycle. Input configuration follows the 12 factor model.
12
+ #
13
+ # The general lifecycle is:
14
+ #
15
+ # * app = App.new()
16
+ # * app.parse_options!
17
+ # * app.run
18
+ class App
19
+ include Rzo::Logging
20
+ include Rzo::OptionParsing
21
+
22
+ ##
23
+ # Exception used to exit the app from a subcommand. Caught by the main run
24
+ # method in the app controller
25
+ class ErrorAndExit < StandardError
26
+ attr_accessor :exit_status
27
+ def initialize(message = nil, exit_status = 1)
28
+ super(message)
29
+ self.exit_status = exit_status
30
+ end
31
+ end
32
+
33
+ ##
34
+ # @param [Array] argv The argument vector, passed to the option parser.
35
+ #
36
+ # @param [Hash] env The environment hash, passed to the option parser to
37
+ # supply defaults not specified on the command line argument vector.
38
+ #
39
+ # @return [App] the application instance.
40
+ def initialize(argv = ARGV.dup, env = ENV.to_hash, stdout = $stdout, stderr = $stderr)
41
+ @argv = argv
42
+ @env = env
43
+ @stdout = stdout
44
+ @stderr = stderr
45
+ reset!
46
+ end
47
+
48
+ ##
49
+ # Reset all state associated with this application instance.
50
+ def reset!
51
+ reset_options!
52
+ reset_logging!(opts)
53
+ @api = nil
54
+ end
55
+
56
+ ##
57
+ # Accessor to Subcommand::Generate
58
+ def generate
59
+ @generate ||= Generate.new(opts, @stdout, @stderr)
60
+ end
61
+
62
+ ##
63
+ # Override this later to allow trollop to write to an intercepted file
64
+ # descriptor for testing. This will avoid trollop's behavior of calling
65
+ # exit()
66
+ def educate
67
+ Trollop.educate
68
+ end
69
+
70
+ ##
71
+ # The main application run method
72
+ #
73
+ # @return [Fixnum] the system exit code
74
+ #
75
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
76
+ def run
77
+ case opts[:subcommand]
78
+ when 'config'
79
+ Config.new(opts, @stdout, @stderr).run
80
+ when 'generate'
81
+ generate.run
82
+ else
83
+ educate
84
+ end
85
+ rescue ErrorAndExit => e
86
+ log.error e.message
87
+ e.backtrace.each { |l| log.debug(l) }
88
+ e.exit_status
89
+ end
90
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
91
+ end
92
+ end
@@ -0,0 +1,144 @@
1
+ require 'logger'
2
+ require 'stringio'
3
+ require 'syslog/logger'
4
+ module Rzo
5
+ ##
6
+ # Support module to mix into a class for consistent logging behavior.
7
+ module Logging
8
+ ##
9
+ # Reset the global logger instance and return it as an object.
10
+ #
11
+ # @return [Logger] initialized logging instance
12
+ def self.reset_logging!(opts)
13
+ logger = opts[:syslog] ? syslog_logger : stream_logger(opts)
14
+ @log = logger
15
+ end
16
+
17
+ ##
18
+ # Return a new Syslog::Logger instance configured for syslog output
19
+ def self.syslog_logger
20
+ # Use the daemon facility, matching Puppet behavior.
21
+ Syslog::Logger.new('rzo', Syslog::LOG_DAEMON)
22
+ end
23
+
24
+ ##
25
+ # Return a new Logger instance configured for file output
26
+ def self.stream_logger(opts)
27
+ out = map_file_option(opts[:logto])
28
+ logger = Logger.new(out)
29
+ logger.level = Logger::WARN
30
+ logger.level = Logger::INFO if opts[:verbose]
31
+ logger.level = Logger::DEBUG if opts[:debug]
32
+ logger
33
+ end
34
+
35
+ ##
36
+ # Logging is handled centrally, the helper methods will delegate to the
37
+ # centrally configured logging instance.
38
+ def self.log
39
+ @log || reset_logging!(opts)
40
+ end
41
+
42
+ ##
43
+ # Map a file option to STDOUT, STDERR or a fully qualified file path.
44
+ #
45
+ # @param [String] filepath A relative or fully qualified file path, or the
46
+ # keyword strings 'STDOUT' or 'STDERR'
47
+ #
48
+ # @return [String] file path or $stdout or $sederr
49
+ def self.map_file_option(filepath)
50
+ case filepath
51
+ when 'STDOUT' then $stdout
52
+ when 'STDERR' then $stderr
53
+ when 'STDIN' then $stdin
54
+ when 'STRING' then StringIO.new
55
+ else File.expand_path(filepath)
56
+ end
57
+ end
58
+
59
+ def map_file_option(filepath)
60
+ ::Rzo::Logging.map_file_option(filepath)
61
+ end
62
+
63
+ def log
64
+ ::Rzo::Logging.log
65
+ end
66
+
67
+ ##
68
+ # Reset the logging system, requires command line options to have been
69
+ # parsed.
70
+ #
71
+ # @param [Hash<Symbol, String>] opts Options hash, passed to the support module
72
+ def reset_logging!(opts)
73
+ ::Rzo::Logging.reset_logging!(opts)
74
+ end
75
+
76
+ ##
77
+ # Logs a message at the fatal (syslog err) log level
78
+ def fatal(msg)
79
+ log.fatal msg
80
+ end
81
+
82
+ ##
83
+ # Logs a message at the error (syslog warning) log level.
84
+ # i.e. May indicate that an error will occur if action is not taken.
85
+ # e.g. A non-root file system has only 2GB remaining.
86
+ def error(msg)
87
+ log.error msg
88
+ end
89
+
90
+ ##
91
+ # Logs a message at the warn (syslog notice) log level.
92
+ # e.g. Events that are unusual, but not error conditions.
93
+ def warn(msg)
94
+ log.warn msg
95
+ end
96
+
97
+ ##
98
+ # Logs a message at the info (syslog info) log level
99
+ # i.e. Normal operational messages that require no action.
100
+ # e.g. An application has started, paused or ended successfully.
101
+ def info(msg)
102
+ log.info msg
103
+ end
104
+
105
+ ##
106
+ # Logs a message at the debug (syslog debug) log level
107
+ # i.e. Information useful to developers for debugging the application.
108
+ def debug(msg)
109
+ log.debug msg
110
+ end
111
+
112
+ ##
113
+ # Helper method to write output, used for stubbing out the tests.
114
+ #
115
+ # @param [String, IO] output the output path or a IO stream
116
+ def write_output(str, output)
117
+ if output.is_a?(IO)
118
+ output.puts(str)
119
+ else
120
+ File.open(output, 'w+') { |f| f.puts(str) }
121
+ end
122
+ end
123
+
124
+ ##
125
+ # Helper method to read from STDIN, or a file and execute an arbitrary block
126
+ # of code. A block must be passed which will recieve an IO object in the
127
+ # event input is a readable file path.
128
+ def input_stream(input)
129
+ if input.is_a?(IO)
130
+ yield input
131
+ else
132
+ File.open(input, 'r') { |stream| yield stream }
133
+ end
134
+ end
135
+
136
+ ##
137
+ # Alternative to puts, writes output to STDERR by default and logs at level
138
+ # info.
139
+ def say(msg)
140
+ log.info(msg)
141
+ @stderr.puts(msg)
142
+ end
143
+ end
144
+ end