cmdb 2.6.2 → 3.0.0rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,15 @@
1
+ version: "2"
2
+ networks:
3
+ default:
4
+ ipam:
5
+ config:
6
+ - subnet: 172.28.0.0/24
7
+ services:
8
+ consul:
9
+ command: "agent -client=172.28.0.2 -bootstrap -server"
10
+ image: consul
11
+ networks:
12
+ default:
13
+ ipv4_address: 172.28.0.2
14
+ ports:
15
+ - "8500"
data/exe/cmdb CHANGED
@@ -20,37 +20,64 @@ CMDB::Commands.constants.each do |konst|
20
20
  commands[name] = CMDB::Commands.const_get(konst.to_sym)
21
21
  end
22
22
 
23
- # Use a Trollop parser for help/banner display, but do not actually parse
24
- # anything just yet.
25
23
  command_list = commands.keys - ['help']
26
24
  command_info = command_list.map { |c| " * #{c}" }.join("\n")
25
+
26
+ # Use a Trollop parser for help/banner display, but do not actually parse
27
+ # anything just yet.
27
28
  p = Trollop::Parser.new do
28
29
  version "cmdb #{gemspec_version} (c) 2013-2014 RightScale, Inc."
29
30
  banner <<-EOS
30
31
  A command-line interface for configuration management.
31
32
 
32
33
  Usage:
33
- cmdb <command> [options]
34
+ cmdb [options] <command> [command-options]
34
35
 
35
36
  Where <command> is one of:
36
37
  #{command_info}
37
38
 
38
- To get help on a command:
39
- cmdb help command
39
+ To get help on a specific command and its options:
40
+ cmdb help <command>
41
+
42
+ Common options that apply to all commands:
40
43
  EOS
41
44
 
45
+ opt :source,
46
+ "Source of CMDB inputs e.g. file:///foo.yml or consul://localhost",
47
+ type: :string,
48
+ multi: true
49
+
50
+ opt :quiet,
51
+ "Suppress unnecessary output",
52
+ default: false
53
+
42
54
  stop_on commands.keys
43
55
  end
44
56
 
45
57
  Trollop.with_standard_exception_handling p do
46
58
  raise Trollop::HelpNeeded if ARGV.empty?
47
- p.parse ARGV
59
+
60
+ # Apply global options
61
+ options = p.parse ARGV
62
+ CMDB.log.level = Logger::FATAL if options[:quiet]
63
+
64
+ if options[:source].empty?
65
+ sources = CMDB::Source.detect
66
+ else
67
+ sources = options[:source].map do |source|
68
+ CMDB::Source.create(source)
69
+ end
70
+ end
71
+
72
+ interface = CMDB::Interface.new(*sources)
73
+
74
+ # Identify the subcommand and run it (or print help).
75
+ raise Trollop::HelpNeeded if ARGV.empty?
48
76
  cmd = ARGV.shift
49
77
  klass = commands[cmd]
50
-
51
78
  if klass
52
- klass.create.run
79
+ klass.create(interface).run
53
80
  else
54
- raise(ArgumentError, "Unrecognized command #{cmd}")
81
+ CMDB.log.fatal "Unrecognized command '#{cmd}'; try 'cmdb --help'"
55
82
  end
56
83
  end
@@ -1,77 +1,32 @@
1
1
  # encoding: utf-8
2
- require 'logger'
3
- require 'listen'
4
2
 
5
3
  module CMDB::Commands
6
4
  class Help
7
- def self.create
8
- options = Trollop.options do
9
- banner <<-EOS
10
- The 'shim' command adapts your applications for use with CMDB without coupling them to
11
- the CMDB RubyGem (or forcing you to write your applications in Ruby). It works by
12
- manipulating the environment or filesystem to make CMDB inputs visible, then invoking
13
- your app.
14
-
15
- To use the shim with apps that read configuration from the filesystem, use the --dir
16
- option to tell the shim where to rewrite configuration files. It will look for tokens
17
- in JSON or YML that look like <<cmdb.key.name>> and replace them with the value of
18
- the specified key.
19
-
20
- To use the shim with 12-factor apps, use the --env option to tell the shim to load
21
- every CMDB key into the environment. When using --env, the prefix of each key is
22
- omitted from the environment variable name, e.g. "common.database.host" is
23
- represented as DATABASE_HOST.
24
-
25
- To support "development mode" and reload your app whenever its files change on disk,
26
- use the --reload option and specify the name of a CMDB key that will enable this
27
- behavior.
28
-
29
- Usage:
30
- cmdb shim [options] -- <command_to_exec> [options_for_command]
31
-
32
- Where [options] are selected from:
33
- EOS
34
- opt :dir,
35
- 'Directory to scan for key-replacement tokens in data files',
36
- type: :string
37
- opt :consul_url,
38
- 'The URL for talking to consul',
39
- type: :string
40
- opt :consul_prefix,
41
- 'The prefix to use when getting keys from consul, can be specified more than once',
42
- type: :string,
43
- multi: true
44
- opt :keys,
45
- 'Override search path(s) for CMDB key files',
46
- type: :strings
47
- opt :pretend,
48
- 'Check for errors, but do not actually launch the app or rewrite files',
49
- default: false
50
- opt :quiet,
51
- "Don't print any output",
52
- default: false
53
- opt :reload,
54
- 'CMDB key that enables reload-on-edit',
55
- type: :string
56
- opt :reload_signal,
57
- 'Signal to send to app server when code is edited',
58
- type: :string,
59
- default: 'HUP'
60
- opt :env,
61
- "Add CMDB keys to the app server's process environment",
62
- default: false
63
- opt :user,
64
- 'Switch to named user before executing app',
65
- type: :string
66
- opt :root,
67
- 'Promote named subkey to the root when it is present in a namespace',
68
- type: :string
69
- end
5
+ def self.create(interface)
6
+ new(interface, ARGV.first)
7
+ end
70
8
 
71
- new(ARGV, options)
9
+ def initialize(interface, command)
10
+ @cmdb = interface
11
+ @command = command
72
12
  end
73
13
 
74
14
  def run
15
+ if @command.nil? || @command.empty?
16
+ # Same as "--help"
17
+ raise Trollop::HelpNeeded
18
+ end
19
+
20
+ # Find the command the user was talking about and print some help
21
+ konst = CMDB::Commands.constants.detect { |konst| konst.to_s.downcase == @command }
22
+ if konst
23
+ klass = CMDB::Commands.const_get(konst)
24
+ ARGV.clear ; ARGV.push('--help')
25
+ klass.create(@cmdb)
26
+ else
27
+ CMDB.log.fatal "CMDB: Unknown command '#{@command}'; try 'cmdb --help' for a list of commands"
28
+ exit 1
29
+ end
75
30
  end
76
31
  end
77
32
  end
@@ -0,0 +1,165 @@
1
+ # encoding: utf-8
2
+
3
+ module CMDB::Commands
4
+ class Shell
5
+ # Character that acts as a separator between key components. The standard
6
+ # notation uses `.` but Shell allow allows `/` for a more filesystem-like
7
+ # user experience.
8
+ ALT_SEPARATOR = '/'.freeze
9
+
10
+ # Directory navigation shortcuts ('.' and '..')
11
+ NAVIGATION = /^#{Regexp.escape(CMDB::SEPARATOR + CMDB::SEPARATOR)}?$/.freeze
12
+
13
+ def self.create(interface)
14
+ require 'cmdb/shell'
15
+
16
+ options = Trollop.options do
17
+ banner <<-EOS
18
+ The 'shell' command enters a Unix-like shell where you can interact with
19
+ CMDB sources and inspect the value of keys.
20
+
21
+ Usage:
22
+ cmdb shell
23
+ EOS
24
+ end
25
+
26
+ new(interface)
27
+ end
28
+
29
+ # @return [CMDB::Interface]
30
+ attr_reader :cmdb
31
+
32
+ # @return [Array] the "present working directory" (i.e. key prefix) for shell commands
33
+ attr_accessor :pwd
34
+
35
+ # @return [Object] esult of the most recent command
36
+ attr_accessor :_
37
+
38
+ # @param [CMDB::Interface] interface
39
+ def initialize(interface)
40
+ @cmdb = interface
41
+ @pwd = []
42
+ @out = CMDB::Shell::Printer.new
43
+ end
44
+
45
+ # Run the shim.
46
+ #
47
+ # @raise [SystemExit] if something goes wrong
48
+ def run
49
+ @dsl = CMDB::Shell::DSL.new(self, @out)
50
+ repl
51
+ end
52
+
53
+ # Given a key name/prefix relative to `pwd`, return the absolute
54
+ # name/prefix. If `pwd` is empty, just return `subpath`. The subpath
55
+ # can use either dotted or slash notation, and directory navigation
56
+ # symbols (`.` and `..`) are applied as expected if the subpath uses
57
+ # slash notation.
58
+ #
59
+ # @return [String] absolute key in dotted notation
60
+ def expand_path(subpath)
61
+ pieces = subpath.split(ALT_SEPARATOR)
62
+
63
+ if subpath[0] == ALT_SEPARATOR
64
+ result = []
65
+ else
66
+ result = pwd.dup
67
+ end
68
+
69
+ if subpath.include?(ALT_SEPARATOR) || subpath =~ NAVIGATION
70
+ # filesystem-like subpath
71
+ # apply Unix-style directory navigation shortcuts
72
+ pieces.each do |piece|
73
+ case piece
74
+ when '..' then result.pop
75
+ when '.' then nil
76
+ else result.push(piece)
77
+ end
78
+ end
79
+
80
+ result.join(CMDB::SEPARATOR)
81
+ else
82
+ # standard dotted notation
83
+ result += pieces
84
+ end
85
+
86
+ result.join(CMDB::SEPARATOR)
87
+ end
88
+
89
+ private
90
+
91
+ def repl
92
+ require 'readline'
93
+ while line = Readline.readline(@out.prompt(self), true)
94
+ begin
95
+ line = line.chomp
96
+ next if line.nil? || line.empty?
97
+ words = line.split(/\s+/)
98
+ command, args = words.first.to_sym, words[1..-1]
99
+
100
+ run_ruby(command, args) || run_getter(line) || run_setter(line) ||
101
+ fail(CMDB::BadCommand.new(command))
102
+ handle_output(self._)
103
+ rescue => e
104
+ handle_error(e) || raise
105
+ end
106
+ end
107
+ rescue Interrupt
108
+ return 0
109
+ end
110
+
111
+ # @return [Boolean] true if line was handled as a normal command
112
+ def run_ruby(command, args)
113
+ self._ = @dsl.__send__(command, *args)
114
+ true
115
+ rescue NoMethodError
116
+ false
117
+ rescue ArgumentError => e
118
+ raise CMDB::BadCommand.new(e.message, command)
119
+ end
120
+
121
+ # @return [Boolean] true if line was handled as a getter
122
+ def run_getter(key)
123
+ if value = @dsl.get(key)
124
+ self._ = value
125
+ true
126
+ else
127
+ false
128
+ end
129
+ rescue CMDB::Error
130
+ false
131
+ end
132
+
133
+ # @return [Boolean] true if line was handled as a setter
134
+ def run_setter(line)
135
+ key, value = line.strip.split(/\s*=\s*/, 2)
136
+ return false unless key && value
137
+
138
+ value = nil unless value && value.length > 0
139
+ self._ = @dsl.set(key, value)
140
+ true
141
+ rescue CMDB::Error
142
+ false
143
+ end
144
+
145
+ def handle_output(obj)
146
+ case obj
147
+ when Hash
148
+ @out.keys_values(obj)
149
+ else
150
+ @out.value(obj)
151
+ end
152
+ end
153
+
154
+ # @return [Boolean] print message and return true if error is handled; else return false
155
+ def handle_error(e)
156
+ case e
157
+ when CMDB::BadCommand
158
+ @out.error "#{e.command}: #{e.message}"
159
+ true
160
+ else
161
+ false
162
+ end
163
+ end
164
+ end
165
+ end
@@ -1,10 +1,8 @@
1
1
  # encoding: utf-8
2
- require 'logger'
3
- require 'listen'
4
2
 
5
3
  module CMDB::Commands
6
4
  class Shim
7
- def self.create
5
+ def self.create(interface)
8
6
  options = Trollop.options do
9
7
  banner <<-EOS
10
8
  The 'shim' command adapts your applications for use with CMDB without coupling them to
@@ -12,7 +10,7 @@ the CMDB RubyGem (or forcing you to write your applications in Ruby). It works b
12
10
  manipulating the environment or filesystem to make CMDB inputs visible, then invoking
13
11
  your app.
14
12
 
15
- To use the shim with apps that read configuration from the filesystem, use the --dir
13
+ To use the shim with apps that read configuration from the filesystem, use the --rewrite
16
14
  option to tell the shim where to rewrite configuration files. It will look for tokens
17
15
  in JSON or YML that look like <<cmdb.key.name>> and replace them with the value of
18
16
  the specified key.
@@ -22,53 +20,28 @@ every CMDB key into the environment. When using --env, the prefix of each key is
22
20
  omitted from the environment variable name, e.g. "common.database.host" is
23
21
  represented as DATABASE_HOST.
24
22
 
25
- To support "development mode" and reload your app whenever its files change on disk,
26
- use the --reload option and specify the name of a CMDB key that will enable this
27
- behavior.
28
-
29
23
  Usage:
30
24
  cmdb shim [options] -- <command_to_exec> [options_for_command]
31
25
 
32
26
  Where [options] are selected from:
33
27
  EOS
34
- opt :dir,
28
+ opt :rewrite,
35
29
  'Directory to scan for key-replacement tokens in data files',
36
30
  type: :string
37
- opt :consul_url,
38
- 'The URL for talking to consul',
39
- type: :string
40
- opt :consul_prefix,
41
- 'The prefix to use when getting keys from consul, can be specified more than once',
42
- type: :string,
43
- multi: true
44
- opt :keys,
45
- 'Override search path(s) for CMDB key files',
46
- type: :strings
47
31
  opt :pretend,
48
32
  'Check for errors, but do not actually launch the app or rewrite files',
49
33
  default: false
50
- opt :quiet,
51
- "Don't print any output",
52
- default: false
53
- opt :reload,
54
- 'CMDB key that enables reload-on-edit',
55
- type: :string
56
- opt :reload_signal,
57
- 'Signal to send to app server when code is edited',
58
- type: :string,
59
- default: 'HUP'
60
34
  opt :env,
61
35
  "Add CMDB keys to the app server's process environment",
62
36
  default: false
63
37
  opt :user,
64
38
  'Switch to named user before executing app',
65
39
  type: :string
66
- opt :root,
67
- 'Promote named subkey to the root when it is present in a namespace',
68
- type: :string
69
40
  end
41
+ options.delete(:help)
70
42
 
71
- new(ARGV, options)
43
+ options.delete_if { |k, v| k.to_s =~ /_given$/i }
44
+ new(interface, ARGV, **options)
72
45
  end
73
46
 
74
47
  # Irrevocably change the current user for this Unix process by calling the
@@ -90,35 +63,18 @@ Where [options] are selected from:
90
63
 
91
64
  # Create a Shim.
92
65
  # @param [Array] command collection of string to pass to Kernel#exec; 0th element is the command name
93
- # @options [String] :condif_dir
94
- def initialize(command, options = {})
66
+ def initialize(interface, command, rewrite:, pretend:, user:)
67
+ @cmdb = interface
95
68
  @command = command
96
- @dir = options[:dir]
97
- @consul_url = options[:consul_url]
98
- @consul_prefixes = options[:consul_prefix]
99
- @keys = options[:keys] || []
100
- @pretend = options[:pretend]
101
- @reload = options[:reload]
102
- @signal = options[:reload_signal]
103
- @env = options[:env]
104
- @user = options[:user]
105
- @root = options[:root]
106
-
107
- CMDB::FileSource.base_directories = @keys unless @keys.empty?
108
- CMDB::ConsulSource.url = @consul_url unless @consul_url.nil?
109
- if !@consul_prefixes.nil? && !@consul_prefixes.empty?
110
- CMDB::ConsulSource.prefixes = @consul_prefixes
111
- end
112
-
113
- CMDB.log.level = Logger::FATAL if options[:quiet]
69
+ @dir = rewrite
70
+ @pretend = pretend
71
+ @user = user
114
72
  end
115
73
 
116
74
  # Run the shim.
117
75
  #
118
76
  # @raise [SystemExit] if something goes wrong
119
77
  def run
120
- @cmdb = CMDB::Interface.new(root: @root)
121
-
122
78
  rewrote = rewrite_files
123
79
  populated = populate_environment
124
80
 
@@ -187,8 +143,6 @@ Where [options] are selected from:
187
143
 
188
144
  # @return [Boolean]
189
145
  def populate_environment
190
- return false unless @env
191
-
192
146
  env = @cmdb.to_h
193
147
 
194
148
  env.keys.each do |k|
@@ -207,81 +161,12 @@ Where [options] are selected from:
207
161
 
208
162
  def launch_app
209
163
  if @command.any?
210
- # log this now, so that it gets logged even if @pretend
211
164
  CMDB.log.info "App will run as user #{@user}" if @user
212
-
213
- if @reload && @cmdb.get(@reload)
214
- CMDB.log.info "SIG#{@signal}-on-edit is enabled; fork app"
215
- fork_and_watch_app unless @pretend
216
- else
217
- CMDB.log.info "SIG#{@signal}-on-edit is disabled; exec app"
218
- exec_app unless @pretend
219
- end
165
+ self.class.drop_privileges(@user) if @user
166
+ exec(*@command)
220
167
  end
221
168
  end
222
169
 
223
- def exec_app
224
- self.class.drop_privileges(@user) if @user
225
- exec(*@command)
226
- end
227
-
228
- def fork_and_watch_app
229
- # let the child share our stdio handles on purpose
230
- pid = fork do
231
- exec_app
232
- end
233
-
234
- CMDB.log.info('App (pid %d) has been forked; watching %s' % [pid, ::Dir.pwd])
235
-
236
- t0 = Time.at(0)
237
-
238
- listener = Listen.to(::Dir.pwd) do |modified, added, removed|
239
- modified = modified.select { |fn| interesting?(fn) }
240
- added = added.select { |fn| interesting?(fn) }
241
- removed = removed.select { |fn| interesting?(fn) }
242
- next if modified.empty? && added.empty? && removed.empty?
243
-
244
- begin
245
- dt = Time.now - t0
246
- if dt > 15
247
- Process.kill(@signal, pid)
248
- CMDB.log.info 'Sent SIG%s to app (pid %d) because (modified,created,deleted)=(%d,%d,%d)' %
249
- [@signal, pid, modified.size, added.size, removed.size]
250
- t0 = Time.now
251
- else
252
- CMDB.log.error 'Skipped SIG%s to app (pid %d) due to timeout (%d)' %
253
- [@signal, pid, dt]
254
- end
255
- rescue
256
- CMDB.log.error 'Skipped SIG%s to app (pid %d) due to %s' %
257
- [@signal, pid, $ERROR_INFO.to_s]
258
- end
259
- end
260
-
261
- listener.start
262
-
263
- wpid = nil
264
- wstatus = nil
265
-
266
- loop do
267
- begin
268
- wpid, wstatus = Process.wait2(-1, Process::WNOHANG)
269
- if wpid
270
- break if wstatus.exited?
271
- CMDB.log.info('Descendant (pid %d) has waited with %s' % [wpid, wstatus.inspect])
272
- end
273
- rescue
274
- CMDB.log.error 'Skipped wait2 to app (pid %d) due to %s' %
275
- [pid, $ERROR_INFO.to_s]
276
- end
277
- sleep(1)
278
- end
279
-
280
- CMDB.log.info('App (pid %d) has exited with %s' % [wpid, wstatus.inspect])
281
- listener.stop
282
- exit(wstatus.exitstatus || 43)
283
- end
284
-
285
170
  def report_rewrite(total)
286
171
  CMDB.log.info "Replaced #{total} variables in #{@dir}"
287
172
  end
data/lib/cmdb/commands.rb CHANGED
@@ -5,4 +5,5 @@ module CMDB
5
5
  end
6
6
 
7
7
  require 'cmdb/commands/help'
8
+ require 'cmdb/commands/shell'
8
9
  require 'cmdb/commands/shim'