cmdb 2.6.2 → 3.0.0rc1

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,31 +3,20 @@ require 'json'
3
3
 
4
4
  module CMDB
5
5
  class Interface
6
- # Create a new instance of the CMDB interface.
7
- # @option settings [String] root name of subkey to consider as root
8
- def initialize(settings = {})
9
- @root = settings[:root] if settings
10
-
11
- namespaces = {}
12
-
13
- load_file_sources(namespaces)
14
- check_overlap(namespaces)
15
-
16
- @sources = []
17
- # Load from consul source first if one is available.
18
- unless ConsulSource.url.nil?
19
- if ConsulSource.prefixes.nil? || ConsulSource.prefixes.empty?
20
- @sources << ConsulSource.new('')
21
- else
22
- ConsulSource.prefixes.each do |prefix|
23
- @sources << ConsulSource.new(prefix)
24
- end
25
- end
26
- end
27
- # Register valid sources with CMDB
28
- namespaces.each do |_, v|
29
- @sources << v.first
6
+ # Create a new instance of the CMDB interface with the specified sources.
7
+ # @param [Array] sources list of String or URI source locations
8
+ # @see Source.create for information on how to specify source URLs
9
+ def initialize(*sources)
10
+ # ensure no two sources share a prefix
11
+ prefixes = {}
12
+ sources.each do |s|
13
+ next if s.prefix.nil?
14
+ prefixes[s.prefix] ||= []
15
+ prefixes[s.prefix] << s
30
16
  end
17
+ check_overlap(prefixes)
18
+
19
+ @sources = sources.dup
31
20
  end
32
21
 
33
22
  # Retrieve the value of a CMDB key, searching all sources in the order they were initialized.
@@ -57,6 +46,23 @@ module CMDB
57
46
  get(key) || raise(MissingKey.new(key))
58
47
  end
59
48
 
49
+ # Set the value of a CMDB key.
50
+ #
51
+ # @return [Source,ni] the source that accepted the write, if any
52
+ # @raise [BadKey] if the key name is malformed
53
+ def set(key, value)
54
+ raise BadKey.new(key) unless key =~ VALID_KEY
55
+
56
+ @sources.reverse.each do |s|
57
+ if s.respond_to?(:set)
58
+ s.set(key, value)
59
+ return s
60
+ end
61
+ end
62
+
63
+ nil
64
+ end
65
+
60
66
  # Enumerate all of the keys in the CMDB.
61
67
  #
62
68
  # @yield every key/value in the CMDB
@@ -71,6 +77,19 @@ module CMDB
71
77
  self
72
78
  end
73
79
 
80
+ def search(prefix)
81
+ prefix = Regexp.new('^' + Regexp.escape(prefix))
82
+ result = {}
83
+
84
+ @sources.each do |s|
85
+ s.each_pair do |k, v|
86
+ result[k] = v if k =~ prefix
87
+ end
88
+ end
89
+
90
+ result
91
+ end
92
+
74
93
  # Transform the entire CMDB into a flat Hash that can be merged into ENV.
75
94
  # Key names are transformed into underscore-separated, uppercase strings;
76
95
  # all runs of non-alphanumeric, non-underscore characters are tranformed
@@ -103,42 +122,15 @@ module CMDB
103
122
 
104
123
  private
105
124
 
106
- # Scan for CMDB data files and index them by namespace
107
- def load_file_sources(namespaces)
108
- # Consult standard base directories for data files
109
- directories = FileSource.base_directories
110
-
111
- # Also consult working dir in development environments
112
- if CMDB.development?
113
- local_dir = File.join(Dir.pwd, '.cmdb')
114
- directories += [local_dir]
115
- end
116
-
117
- directories.each do |dir|
118
- (Dir.glob(File.join(dir, '*.js')) + Dir.glob(File.join(dir, '*.json'))).each do |filename|
119
- source = FileSource.new(filename, @root)
120
- namespaces[source.prefix] ||= []
121
- namespaces[source.prefix] << source
122
- end
123
-
124
- (Dir.glob(File.join(dir, '*.yml')) + Dir.glob(File.join(dir, '*.yaml'))).each do |filename|
125
- source = FileSource.new(filename, @root)
126
- namespaces[source.prefix] ||= []
127
- namespaces[source.prefix] << source
128
- end
129
- end
130
- end
131
-
132
- # Check for overlapping namespaces and react appropriately. This can happen when a file
133
- # of a given name is located in more than one of the key-search directories. We tolerate
134
- # this in development mode, but raise an exception otherwise.
135
- def check_overlap(namespaces)
136
- overlapping = namespaces.select { |_, sources| sources.size > 1 }
137
- overlapping.each do |ns, sources|
138
- exc = ValueConflict.new(ns, sources)
125
+ # Check that no two sources share a prefix. Raise an exception if any
126
+ # overlap is detected.
127
+ def check_overlap(prefix_sources)
128
+ overlapping = prefix_sources.select { |_, sources| sources.size > 1 }
129
+ overlapping.each do |p, sources|
130
+ exc = ValueConflict.new(p, sources)
139
131
 
140
- CMDB.log.warn exc.message
141
- raise exc unless CMDB.development?
132
+ CMDB.log.error exc.message
133
+ raise exc
142
134
  end
143
135
  end
144
136
 
@@ -0,0 +1,73 @@
1
+ module CMDB::Shell
2
+ # Host for CMDB command methods. Every public method of this class is
3
+ # a CMDB command and its parameters represent the arguments to the
4
+ # command. If a command is successful, it always updates the `_` attribute
5
+ # with the output (return value) of the command.
6
+ class DSL < BasicObject
7
+ def initialize(shell, out)
8
+ @shell = shell
9
+ @cmdb = @shell.cmdb
10
+ @out = out
11
+ end
12
+
13
+ def ls(path='')
14
+ prefix = @shell.expand_path(path)
15
+ @cmdb.search prefix
16
+ end
17
+
18
+ def help
19
+ @out.info 'Commands:'
20
+ @out.info ' cd slash/sep/path - append to search prefix'
21
+ @out.info ' cd /path - reset prefix'
22
+ @out.info ' get <key> - print value of key'
23
+ @out.info ' ls - show keys and values'
24
+ @out.info ' set <key> <value> - print value of key'
25
+ @out.info ' quit - exit the shell'
26
+ @out.info 'Notation:'
27
+ @out.info ' a.b.c - (sub)key relative to search prefix'
28
+ @out.info ' ../b/c - the b.c subkey relative to parent of search prefix'
29
+ @out.info 'Shortcuts:'
30
+ @out.info ' <key> - for get'
31
+ @out.info ' <key>=<value> - for set'
32
+ @out.info ' cat,rm,unset,... - as expected'
33
+ end
34
+
35
+ def get(key)
36
+ key = @shell.expand_path(key)
37
+
38
+ @cmdb.get(key)
39
+ end
40
+ alias cat get
41
+
42
+ def set(key, value)
43
+ key = @shell.expand_path key
44
+
45
+ if @cmdb.set(key, value)
46
+ @cmdb.get(key)
47
+ else
48
+ ::Kernel.raise ::CMDB::BadCommand.new('set', 'No source is capable of accepting writes')
49
+ end
50
+ end
51
+
52
+ def unset(key)
53
+ @cmdb.set(key, nil)
54
+ end
55
+ alias rm unset
56
+
57
+ def cd(path)
58
+ pwd = @shell.expand_path(path)
59
+ @shell.pwd = pwd.split(::CMDB::SEPARATOR)
60
+ pwd.to_sym
61
+ end
62
+ alias chdir cd
63
+
64
+ def pry
65
+
66
+ end
67
+
68
+ def quit
69
+ ::Kernel.raise ::Interrupt
70
+ end
71
+ alias exit quit
72
+ end
73
+ end
@@ -0,0 +1,115 @@
1
+ module CMDB::Shell
2
+ class Printer
3
+ def initialize(out=STDOUT, err=STDERR)
4
+ @out = out
5
+ @err = err
6
+ @c = Text.new(!@out.tty?)
7
+ end
8
+
9
+ # Print an informational message.
10
+ def info(str)
11
+ @out.puts @c.white(str)
12
+ end
13
+
14
+ # Print an error message.
15
+ def error(str)
16
+ @err.puts @c.bright_red(str)
17
+ self
18
+ end
19
+
20
+ # Display a single CMDB value.
21
+ def value(obj)
22
+ @out.puts ' ' + color_value(obj, 76)
23
+ self
24
+ end
25
+
26
+ # Display a table of keys/values.
27
+ def keys_values(h, prefix:nil)
28
+ wk = h.keys.inject(0) { |ax, e| e.size > ax ? e.size : ax }
29
+ wv = h.values.inject(0) { |ax, e| es = e.inspect.size; es > ax ? es : ax }
30
+ width = @c.tty_columns
31
+ half = width / 2 - 2
32
+ wk = [wk, half].min
33
+ wv = [wv, half].min
34
+ re = (width - wk - wv)
35
+ wv += re if re > 0
36
+
37
+ h.each do |k, v|
38
+ @out.puts format(' %s%s', color_key(k, wk+1, prefix:prefix), color_value(v, wv))
39
+ end
40
+
41
+ self
42
+ end
43
+
44
+ # @return [String] human-readable CMDB prompt
45
+ def prompt(cmdb)
46
+ pwd = '/' + cmdb.pwd.join('/')
47
+ pwd = pwd[0...40] + '...' if pwd.size >= 40
48
+ 'cmdb:' +
49
+ @c.green(pwd) +
50
+ @c.default('> ')
51
+ end
52
+
53
+ private
54
+
55
+ # Colorize a key and right-pad it to fit a minimum size. Append a ':'
56
+ # to make it YAML-esque.
57
+ def color_key(k, size, prefix:nil)
58
+ v = k.to_s
59
+ v.sub(prefix, '') if prefix && v.index(prefix) == 0
60
+ suffix = ':'
61
+ if v.size + 1 > size
62
+ v = v[0...size-4]
63
+ suffix = '...:'
64
+ end
65
+ pad = [0, size - v.size - suffix.size].max
66
+ @c.blue(v) << suffix << (' ' * pad)
67
+ end
68
+
69
+ # Colorize a value and right-pad it to fit a minimum size.
70
+ def color_value(v, size)
71
+ case v
72
+ when Symbol
73
+ vv = v.to_s
74
+ when nil
75
+ vv = 'null'
76
+ else
77
+ vv = v.inspect
78
+ end
79
+ vv << (' ' * [0, size - vv.size].max)
80
+
81
+ case v
82
+ when Symbol
83
+ @c.blue(vv)
84
+ when String
85
+ @c.bright_green(vv)
86
+ when Numeric
87
+ @c.bright_magenta(vv)
88
+ when true, false
89
+ @c.cyan(vv)
90
+ when nil
91
+ @c.yellow(vv)
92
+ when Array
93
+ str = @c.bold('[')
94
+ remain = size-2
95
+ v.each_with_index do |e, i|
96
+ ei = e.inspect
97
+ if remain >= ei.size + 3
98
+ str << ',' if i > 0
99
+ str << color_value(e, ei.size)
100
+ elsif remain >= 3
101
+ str << @c.default('...')
102
+ remain = 1
103
+ end
104
+ remain -= (ei.size + 1)
105
+ end
106
+ str << @c.bold(']')
107
+
108
+ str
109
+ else
110
+ @c.default(vv)
111
+ end
112
+
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,65 @@
1
+ module CMDB::Shell
2
+ # Adapted from pry: https://github.com/pry/pry
3
+ class Text
4
+ COLORS = {
5
+ 'black' => 0,
6
+ 'red' => 1,
7
+ 'green' => 2,
8
+ 'yellow' => 3,
9
+ 'blue' => 4,
10
+ 'purple' => 5,
11
+ 'magenta' => 5,
12
+ 'cyan' => 6,
13
+ 'white' => 7
14
+ }.freeze
15
+
16
+ def initialize(plain)
17
+ @plain = plain
18
+ end
19
+
20
+ COLORS.each_pair do |color, value|
21
+ define_method color do |text|
22
+ @plain && text || "\033[0;#{30+value}m#{text}\033[0m"
23
+ end
24
+
25
+ define_method "bright_#{color}" do |text|
26
+ @plain && text || "\033[1;#{30+value}m#{text}\033[0m"
27
+ end
28
+ end
29
+
30
+ # Remove any color codes from _text_.
31
+ #
32
+ # @param [String, #to_s] text
33
+ # @return [String] _text_ stripped of any color codes.
34
+ def strip_color(text)
35
+ text.to_s.gsub(/\e\[.*?(\d)+m/ , '')
36
+ end
37
+
38
+ # Returns _text_ as bold text for use on a terminal.
39
+ #
40
+ # @param [String, #to_s] text
41
+ # @return [String] _text_
42
+ def bold(text)
43
+ @plain && text || "\e[1m#{text}\e[0m"
44
+ end
45
+
46
+ # Returns `text` in the default foreground colour.
47
+ # Use this instead of "black" or "white" when you mean absence of colour.
48
+ #
49
+ # @param [String, #to_s] text
50
+ # @return [String]
51
+ def default(text)
52
+ text.to_s
53
+ end
54
+ alias_method :bright_default, :bold
55
+
56
+ # @return [Integer] screen width (number of columns)
57
+ def tty_columns
58
+ if @plain
59
+ 65_535
60
+ else
61
+ Integer(`stty size`.chomp.split(/ +/)[1]) rescue 80
62
+ end
63
+ end
64
+ end
65
+ end
data/lib/cmdb/shell.rb ADDED
@@ -0,0 +1,8 @@
1
+ module CMDB
2
+ module Shell
3
+ end
4
+ end
5
+
6
+ require 'cmdb/shell/dsl'
7
+ require 'cmdb/shell/text'
8
+ require 'cmdb/shell/printer'
@@ -0,0 +1,90 @@
1
+ # encoding: utf-8
2
+ require 'base64'
3
+ require 'net/http'
4
+ require 'open-uri'
5
+
6
+ module CMDB
7
+ class Source::Consul < Source::Network
8
+ # Regular expression to match array values
9
+ ARRAY_VALUE = /^\[(.*)\]$/
10
+
11
+ # Get a single key from consul. If the key is not found, return nil.
12
+ #
13
+ # @param [String] key dot-notation key
14
+ # @return [Object]
15
+ def get(key)
16
+ return nil unless prefixed?(key)
17
+ key = dot_to_slash(key)
18
+ response = http_get path_to(key)
19
+ case response
20
+ when String
21
+ response = json_parse(response)
22
+ item = response.first
23
+ json_parse(Base64.decode64(item['Value']))
24
+ when 404
25
+ nil
26
+ else
27
+ raise CMDB:Error.new("Unexpected consul response #{value.inspect}")
28
+ end
29
+ end
30
+
31
+ # Set a single key in consul. If value is nil, then delete the key
32
+ # entirely from consul.
33
+ #
34
+ # @param [String] key dot-notation key
35
+ # @param [Object] value new value of key
36
+ def set(key, value)
37
+ key = dot_to_slash(key)
38
+ if value.nil?
39
+ http_delete path_to(key)
40
+ else
41
+ http_put path_to(key), value
42
+ end
43
+ end
44
+
45
+ # Iterate through all keys in this source.
46
+ # @return [Integer] number of key/value pairs that were yielded
47
+ def each_pair(&_block)
48
+ path = path_to('/')
49
+
50
+ case result = http_get(path, query:'recurse')
51
+ when String
52
+ result = json_parse(result)
53
+ when 404
54
+ return # no keys!
55
+ end
56
+
57
+ unless result.is_a?(Array)
58
+ raise CMDB::Error.new("Consul 'GET #{path}': expected Array, got #{all.class.name}")
59
+ end
60
+
61
+ result.each do |item|
62
+ key = slash_to_dot(item['Key'])
63
+ value = json_parse(Base64.decode64(item['Value']))
64
+ yield(key, value)
65
+ end
66
+
67
+ result.size
68
+ end
69
+
70
+ # Test connectivity to consul agent.
71
+ #
72
+ # @return [Boolean]
73
+ def ping
74
+ http_get('/') == 'Consul Agent'
75
+ rescue
76
+ false
77
+ end
78
+
79
+ private
80
+
81
+ # Given a key's relative path, return its absolute REST path in the consul
82
+ # kv, including prefix if appropriate.
83
+ def path_to(subkey)
84
+ p = '/v1/kv/'
85
+ p << prefix << '/' if prefix
86
+ p << subkey unless (subkey == '/' && p[-1] == '/')
87
+ p
88
+ end
89
+ end
90
+ end
@@ -2,42 +2,25 @@
2
2
  require 'uri'
3
3
 
4
4
  module CMDB
5
- # Data source that is backed by a YAML file that lives in the filesystem. The name of the YAML
6
- # file becomes the top-level key under which all values in the YAML are exposed, preserving
7
- # their exact structure as parsed by YAML.
5
+ # Data source that is backed by a YAML/JSON file that lives in the filesystem. The name of the
6
+ # file becomes the top-level key under which all values in the file are exposed, preserving
7
+ # their exact structure as parsed by YAML/JSON.
8
8
  #
9
9
  # @example Use my.yml as a CMDB source
10
- # source = FileSource.new('/tmp/my.yml') # contains a top-level stanza named "database"
10
+ # source = Source::File.new('/tmp/my.yml') # contains a top-level stanza named "database"
11
11
  # source['my']['database']['host'] # => 'db1-1.example.com'
12
- class FileSource
13
- # @return [URI] a file:// URL describing where this source's data comes from
14
- attr_reader :prefix, :url
15
-
16
- @base_directories = ['/var/lib/cmdb', File.expand_path('~/.cmdb')]
17
-
18
- # List of directories that will be searched (in order) for YML files at load time.
19
- # @return [Array] collection of String
20
- class << self
21
- attr_reader :base_directories
22
- end
23
-
24
- # @param [Array] bd collection of String absolute paths to search for YML files
25
- class << self
26
- attr_writer :base_directories
27
- end
28
-
29
- # Construct a new FileSource from an input YML file.
30
- # @param [String,Pathname] filename path to a YAML file
31
- # @param [String] root optional subpath in data to "mount"
12
+ class Source::File < Source
13
+ # Construct a new Source::File from an input file.
14
+ # @param [String,Pathname] filename path to a file
32
15
  # @param [String] prefix optional prefix of
33
16
  # @raise [BadData] if the file's content is malformed
34
- def initialize(filename, root = nil, prefix = File.basename(filename, '.*'))
17
+ def initialize(filename, prefix)
35
18
  @data = {}
36
19
  @prefix = prefix
37
- filename = File.expand_path(filename)
20
+ filename = ::File.expand_path(filename)
38
21
  @url = URI.parse("file://#{filename}")
39
- @extension = File.extname(filename)
40
- raw_bytes = File.read(filename)
22
+ @extension = ::File.extname(filename)
23
+ raw_bytes = ::File.read(filename)
41
24
  raw_data = nil
42
25
 
43
26
  begin
@@ -53,32 +36,30 @@ module CMDB
53
36
  raise BadData.new(url, 'CMDB data file')
54
37
  end
55
38
 
56
- raw_data = raw_data[root] if !root.nil? && raw_data.key?(root)
57
39
  flatten(raw_data, @prefix, @data)
58
40
  end
59
41
 
60
42
  # Get the value of key.
61
43
  #
62
- # @return [nil,String,Numeric,TrueClass,FalseClass,Array] the key's value, or nil if not found
44
+ # @return [Object] the key's value, or nil if not found
63
45
  def get(key)
64
46
  @data[key]
65
47
  end
66
48
 
67
- # Enumerate the keys in this source, and their values.
49
+ # Enumerate the keys and values in this source.
68
50
  #
69
51
  # @yield every key/value in the source
70
52
  # @yieldparam [String] key
71
53
  # @yieldparam [Object] value
72
54
  def each_pair(&_block)
73
- # Strip the prefix in the key and call the block
74
- @data.each_pair { |k, v| yield(k.split("#{@prefix}.").last, v) }
55
+ @data.each_pair { |k, v| yield(k, v) }
75
56
  end
76
57
 
77
58
  private
78
59
 
79
60
  def flatten(data, prefix, output)
80
61
  data.each_pair do |key, value|
81
- key = "#{prefix}.#{key}"
62
+ key = "#{prefix}#{CMDB::SEPARATOR}#{key}"
82
63
  case value
83
64
  when Hash
84
65
  flatten(value, key, output)
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+ require 'uri'
3
+
4
+ module CMDB
5
+ # Data source that is backed by the an in-memory Ruby hash; used solely for
6
+ # testing.
7
+ class Source::Memory
8
+ # @return [String] the empty string
9
+ attr_reader :prefix
10
+
11
+ # Construct a new Source::Hahs.
12
+ def initialize(hash, prefix)
13
+ @hash = hash
14
+ @prefix = prefix
15
+ end
16
+
17
+ # Get the value of key.
18
+ #
19
+ # @return [nil,String,Numeric,TrueClass,FalseClass,Array] the key's value, or nil if not found
20
+ def get(key)
21
+ @hash[key]
22
+ end
23
+
24
+ # Set the value of a key.
25
+ def set(key, value)
26
+ value = JSON.dump(value) unless value.is_a?(String)
27
+ @hash[key] = value
28
+ end
29
+
30
+ # Enumerate the keys in this source, and their values.
31
+ #
32
+ # @yield every key/value in the source
33
+ # @yieldparam [String] name of key
34
+ # @yieldparam [Object] value of key
35
+ def each_pair(&block)
36
+ @hash.each_pair(&block)
37
+ end
38
+ end
39
+ end