cmdb 2.6.2 → 3.0.0rc1

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.
@@ -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