cmdb 0.1.0 → 2.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -3
- data/Gemfile.lock +1 -1
- data/README.md +51 -31
- data/cmdb.gemspec +8 -1
- data/exe/cmdb +53 -0
- data/lib/cmdb/commands/help.rb +75 -0
- data/lib/cmdb/commands/shim.rb +298 -0
- data/lib/cmdb/commands.rb +7 -0
- data/lib/cmdb/consul_source.rb +77 -0
- data/lib/cmdb/file_source.rb +102 -0
- data/lib/cmdb/interface.rb +165 -0
- data/lib/cmdb/rewriter.rb +141 -0
- data/lib/cmdb/version.rb +3 -0
- data/lib/cmdb.rb +138 -0
- metadata +14 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: df72629dd0ba9fcb24a33607ca6216e9a70ff188
|
4
|
+
data.tar.gz: 2daf8320e35c3cd58234d86b452b8574cc76e944
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 97c03113ce4aa1c50718edadd6c9649096a22916bf14a16721df59987f56745c8150131a5bbb63d6d6f2c3c9e4db59fff2568f2480e0e3488468400b4cc92253
|
7
|
+
data.tar.gz: 38cc4d3c2701dfc66b2d3f27470851bd14128e5ec4b3548e85fa292daced4d609cb43f4d63746a73e449cd410e78f109ea27f8f822b99e73a0d4a45f4fa6fd7c
|
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,47 +1,50 @@
|
|
1
1
|
# CMDB
|
2
2
|
|
3
|
-
[![TravisCI][travis_ci_img]](https://
|
4
|
-
[travis_ci_img]: https://
|
3
|
+
[![TravisCI][travis_ci_img]](https://travis-ci.org/rightscale/cmdb)
|
4
|
+
[travis_ci_img]: https://travis-ci.org/rightscale/cmdb.svg?branch=master
|
5
|
+
|
6
|
+
*NOTE:* this gem is under heavy development and it is likely that v3 will contain several interface-breaking
|
7
|
+
changes and simplifications. We encourage you to play with our toys and give us feedback on how you would
|
8
|
+
like to see the project evolve, but if you use this gem for production-grade software, please make sure to
|
9
|
+
pin to version `~> 2.6` in your Gemfile to avoid breakage!
|
5
10
|
|
6
11
|
CMDB is a Ruby interface for consuming data from one or more configuration management databases
|
7
|
-
(CMDBs)
|
12
|
+
(CMDBs) and making that information available to Web applications.
|
13
|
+
|
14
|
+
It is intended to support multiple CM technologies, including:
|
8
15
|
- JSON/YAML files on a local disk
|
9
16
|
- consul
|
10
17
|
- (someday) etcd
|
11
18
|
- (someday) ZooKeeper
|
12
19
|
|
13
|
-
The CMDB's command-line tool can also facilitate debugging by watching your application's files
|
14
|
-
and sending SIGHUP (or the signal of your choice) to the app server if anything changes.
|
15
|
-
|
16
20
|
Maintained by
|
17
21
|
- [RightScale Inc.](https://www.rightscale.com)
|
18
22
|
|
19
23
|
## Why should I use this gem?
|
20
24
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
25
|
+
CMDB supports two primary use cases:
|
26
|
+
|
27
|
+
1. Decouple your modern (12-factor) application from the CM mechanism that is used to deploy it,
|
28
|
+
transforming CMDB keys and values into the enviroment variables that your app expects.
|
29
|
+
2. Help you deploy your "legacy" application that expects its configuration to be written to
|
30
|
+
disk files, rewriting those files with data taken from the CMDB.
|
26
31
|
|
27
32
|
The gem has two primary interfaces:
|
28
|
-
- The `cmdb shim` command populates the environment with
|
33
|
+
- The `cmdb shim` command populates the environment with values and/or rewrites hardcoded
|
29
34
|
config files, then spawns your application. It can also be told to watch the filesystem for changes and
|
30
35
|
send a signal e.g. `SIGHUP` to your application, bringing reload-on-edit functionality to any app.
|
31
|
-
- The `CMDB::Interface` object provides a programmatic API for querying
|
36
|
+
- The `CMDB::Interface` object provides a programmatic API for querying CMDBs. Its `#to_h`
|
32
37
|
method transforms the whole configuration into an environment-friendly hash if you prefer to seed the
|
33
|
-
environment yourself.
|
34
|
-
|
35
|
-
The CMDB gem is friendly to 12-factor and "Rails-style" apps and can be used with or without the shim,
|
36
|
-
depending on your application's needs.
|
38
|
+
environment yourself, without using the shim.
|
37
39
|
|
38
40
|
# Getting Started
|
39
41
|
|
40
42
|
## Create CMDB Data Files
|
41
|
-
|
43
|
+
|
42
44
|
The shim looks in two locations to find data files. In order of precedence:
|
43
|
-
|
44
|
-
|
45
|
+
|
46
|
+
1. `/var/lib/cmdb` -- typically at deployment time
|
47
|
+
2. `~/.cmdb` -- useful for developers when testing the app
|
45
48
|
|
46
49
|
The base name (minus extension) of each file is important; it determines the top-level name of
|
47
50
|
the keys in that file and it *must be unique* across all of the directories. For instance,
|
@@ -59,7 +62,7 @@ the following contents:
|
|
59
62
|
|
60
63
|
## Invoke the CMDB Shim
|
61
64
|
|
62
|
-
For non-Ruby applications, or for situations where CMDB values are required
|
65
|
+
For non-Ruby applications, or for situations where CMDB values are required
|
63
66
|
outside of the context of interpreted code, use `cmdb shim` to run
|
64
67
|
your application. The shim can do several things for you:
|
65
68
|
|
@@ -83,8 +86,8 @@ for CMDB values that are serialized to the environment (as a JSON document, in t
|
|
83
86
|
|
84
87
|
### Rewriting configuration files with CMDB values
|
85
88
|
|
86
|
-
If the `--dir` option is provided, the shim recursively scans your working
|
87
|
-
directory (`Dir.pwd`) for data files that contain replacement tokens; when a token is
|
89
|
+
If the `--dir` option is provided, the shim recursively scans your working
|
90
|
+
directory (`Dir.pwd`) for data files that contain replacement tokens; when a token is
|
88
91
|
found, it substitutes the corresponding CMDB key's value.
|
89
92
|
|
90
93
|
Replacement tokens look like this: `<<name.of.my.key>>` and can appear anywhere in a file as a YAML
|
@@ -93,7 +96,7 @@ or JSON _value_ (but never a key).
|
|
93
96
|
Replacement tokens should appear inside string literals in your configuration files so they don't
|
94
97
|
invalidate syntax or render the files unparsable by other tools.
|
95
98
|
|
96
|
-
The shim performs replacement in-memory and saves all of the edits at once, making the rewrite
|
99
|
+
The shim performs replacement in-memory and saves all of the edits at once, making the rewrite
|
97
100
|
operation nearly atomic. If any keys are missing, then no files are changed on disk and the shim
|
98
101
|
exits with a helpful error message.
|
99
102
|
|
@@ -115,7 +118,7 @@ I can run the following command in my application's root directory:
|
|
115
118
|
|
116
119
|
bundle exec cmdb shim --dir=config rackup
|
117
120
|
|
118
|
-
This will rewrite the files under config, replacing my configuration files as
|
121
|
+
This will rewrite the files under config, replacing my configuration files as
|
119
122
|
follows:
|
120
123
|
|
121
124
|
# config/database.yml
|
@@ -172,6 +175,27 @@ value of a CMDB key can be a string, boolean, number, nil, or a list of any of t
|
|
172
175
|
|
173
176
|
CMDB keys *cannot* contain maps/hashes, nor can lists contain differently-typed data.
|
174
177
|
|
178
|
+
When a CMDB key is accessed through the Ruby API or referenced with a file-rewrite <<token>>, its
|
179
|
+
name always begins with the file or path name of its *source* (JSON file, consul path, etc).
|
180
|
+
|
181
|
+
When a CMDB key is written into the process environment or accessed via `Source#to_h`, its name
|
182
|
+
is "bare" and the source name is irrelevant.
|
183
|
+
|
184
|
+
If we use a `--consul-prefix` of `/kv/rightscale/intregration/shard403/common`
|
185
|
+
then a key names would look like `common.debug.enabled` and environment names
|
186
|
+
would look like `DEBUG_ENABLED`. The same is true if we load a `common.json`
|
187
|
+
file source from `/var/lib/cmdb`.
|
188
|
+
|
189
|
+
A future version of cmdb will harmonize the treatment of names; the prefix
|
190
|
+
will be insignificant to the key name and keys will look like environment
|
191
|
+
variables.
|
192
|
+
|
193
|
+
## Network Data Sources
|
194
|
+
|
195
|
+
To read from a consul server, pass `--consul-url` with a consul server address
|
196
|
+
and `--consul-prefix` one or more times with a top-level path to treat as a
|
197
|
+
named source.
|
198
|
+
|
175
199
|
## Disk-Based Data Sources
|
176
200
|
|
177
201
|
When the CMDB interface is initialized, it searches two directories for YAML files:
|
@@ -212,10 +236,10 @@ on the value of RACK_ENV or RAILS_ENV:
|
|
212
236
|
- unset, development or test: CMDB chooses the highest-precedence file and ignores the others
|
213
237
|
after printing a warning. Files in `/etc` win over files in `$HOME`, which win over
|
214
238
|
files in the working directory.
|
215
|
-
|
239
|
+
|
216
240
|
- any other environment: CMDB fails with an error message that describes the problem and
|
217
241
|
the locations of the overlapping files.
|
218
|
-
|
242
|
+
|
219
243
|
### Ambiguous Key Names
|
220
244
|
|
221
245
|
Consider a file that defines the following variables:
|
@@ -244,7 +268,3 @@ of the keys could change if the structure of the YML file changes.
|
|
244
268
|
For this reason, any YAML file that defines an "ambiguous" key name will cause an error at
|
245
269
|
initialization time. To avoid ambiguous key names, think of your YAML file as a tree and remember
|
246
270
|
that _leaf nodes must define data_ and _internal nodes must define structure_.
|
247
|
-
|
248
|
-
## Network Data Sources
|
249
|
-
|
250
|
-
TODO: add support for etcd or similar
|
data/cmdb.gemspec
CHANGED
@@ -1,6 +1,11 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'cmdb/version'
|
5
|
+
|
1
6
|
Gem::Specification.new do |spec|
|
2
7
|
spec.name = 'cmdb'
|
3
|
-
spec.version =
|
8
|
+
spec.version = CMDB::VERSION
|
4
9
|
spec.authors = ['RightScale']
|
5
10
|
spec.email = ['rubygems@rightscale.com']
|
6
11
|
|
@@ -10,6 +15,8 @@ Gem::Specification.new do |spec|
|
|
10
15
|
spec.license = 'MIT'
|
11
16
|
|
12
17
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
13
20
|
spec.require_paths = ['lib']
|
14
21
|
|
15
22
|
spec.required_ruby_version = Gem::Requirement.new("~> 2.0")
|
data/exe/cmdb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'trollop'
|
5
|
+
require 'cmdb'
|
6
|
+
|
7
|
+
if gemspec = Gem.loaded_specs['cmdb']
|
8
|
+
gemspec_version = gemspec.version
|
9
|
+
else
|
10
|
+
require_relative '../lib/cmdb/version'
|
11
|
+
gemspec_version = CMDB::VERSION
|
12
|
+
end
|
13
|
+
|
14
|
+
commands = {}
|
15
|
+
CMDB::Commands.constants.each do |konst|
|
16
|
+
name = konst.to_s.downcase
|
17
|
+
commands[name] = CMDB::Commands.const_get(konst.to_sym)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Use a Trollop parser for help/banner display, but do not actually parse
|
21
|
+
# anything just yet.
|
22
|
+
command_list = commands.keys - ['help']
|
23
|
+
command_info = command_list.map { |c| " * #{c}" }.join("\n")
|
24
|
+
p = Trollop::Parser.new do
|
25
|
+
version "cmdb #{gemspec_version} (c) 2013-2014 RightScale, Inc."
|
26
|
+
banner <<-EOS
|
27
|
+
A command-line interface for configuration management.
|
28
|
+
|
29
|
+
Usage:
|
30
|
+
cmdb <command> [options]
|
31
|
+
|
32
|
+
Where <command> is one of:
|
33
|
+
#{command_info}
|
34
|
+
|
35
|
+
To get help on a command:
|
36
|
+
cmdb help command
|
37
|
+
EOS
|
38
|
+
|
39
|
+
stop_on commands.keys
|
40
|
+
end
|
41
|
+
|
42
|
+
opts = Trollop::with_standard_exception_handling p do
|
43
|
+
raise Trollop::HelpNeeded if ARGV.empty?
|
44
|
+
p.parse ARGV
|
45
|
+
cmd = ARGV.shift
|
46
|
+
klass = commands[cmd]
|
47
|
+
|
48
|
+
if klass
|
49
|
+
klass.create.run
|
50
|
+
else
|
51
|
+
raise ArgumentError, "Unrecognized command #{cmd}"
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'listen'
|
3
|
+
|
4
|
+
module CMDB::Commands
|
5
|
+
class Help
|
6
|
+
def self.create
|
7
|
+
options = Trollop.options do
|
8
|
+
banner <<-EOS
|
9
|
+
The 'shim' command adapts your applications for use with CMDB without coupling them to
|
10
|
+
the CMDB RubyGem (or forcing you to write your applications in Ruby). It works by
|
11
|
+
manipulating the environment or filesystem to make CMDB inputs visible, then invoking
|
12
|
+
your app.
|
13
|
+
|
14
|
+
To use the shim with apps that read configuration from the filesystem, use the --dir
|
15
|
+
option to tell the shim where to rewrite configuration files. It will look for tokens
|
16
|
+
in JSON or YML that look like <<cmdb.key.name>> and replace them with the value of
|
17
|
+
the specified key.
|
18
|
+
|
19
|
+
To use the shim with 12-factor apps, use the --env option to tell the shim to load
|
20
|
+
every CMDB key into the environment. When using --env, the prefix of each key is
|
21
|
+
omitted from the environment variable name, e.g. "common.database.host" is
|
22
|
+
represented as DATABASE_HOST.
|
23
|
+
|
24
|
+
To support "development mode" and reload your app whenever its files change on disk,
|
25
|
+
use the --reload option and specify the name of a CMDB key that will enable this
|
26
|
+
behavior.
|
27
|
+
|
28
|
+
Usage:
|
29
|
+
cmdb shim [options] -- <command_to_exec> [options_for_command]
|
30
|
+
|
31
|
+
Where [options] are selected from:
|
32
|
+
EOS
|
33
|
+
opt :dir,
|
34
|
+
"Directory to scan for key-replacement tokens in data files",
|
35
|
+
:type => :string
|
36
|
+
opt :consul_url,
|
37
|
+
"The URL for talking to consul",
|
38
|
+
:type => :string
|
39
|
+
opt :consul_prefix,
|
40
|
+
"The prefix to use when getting keys from consul, can be specified more than once",
|
41
|
+
:type => :string,
|
42
|
+
:multi => true
|
43
|
+
opt :keys,
|
44
|
+
"Override search path(s) for CMDB key files",
|
45
|
+
:type => :strings
|
46
|
+
opt :pretend,
|
47
|
+
"Check for errors, but do not actually launch the app or rewrite files",
|
48
|
+
:default => false
|
49
|
+
opt :quiet,
|
50
|
+
"Don't print any output",
|
51
|
+
:default => false
|
52
|
+
opt :reload,
|
53
|
+
"CMDB key that enables reload-on-edit",
|
54
|
+
:type => :string
|
55
|
+
opt :reload_signal,
|
56
|
+
"Signal to send to app server when code is edited",
|
57
|
+
:type => :string,
|
58
|
+
:default => "HUP"
|
59
|
+
opt :env,
|
60
|
+
"Add CMDB keys to the app server's process environment",
|
61
|
+
:default => false
|
62
|
+
opt :user,
|
63
|
+
"Switch to named user before executing app",
|
64
|
+
:type => :string
|
65
|
+
opt :root,
|
66
|
+
"Promote named subkey to the root when it is present in a namespace",
|
67
|
+
:type => :string
|
68
|
+
end
|
69
|
+
|
70
|
+
self.new(ARGV, options)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,298 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'listen'
|
3
|
+
|
4
|
+
module CMDB::Commands
|
5
|
+
class Shim
|
6
|
+
def self.create
|
7
|
+
options = Trollop.options do
|
8
|
+
banner <<-EOS
|
9
|
+
The 'shim' command adapts your applications for use with CMDB without coupling them to
|
10
|
+
the CMDB RubyGem (or forcing you to write your applications in Ruby). It works by
|
11
|
+
manipulating the environment or filesystem to make CMDB inputs visible, then invoking
|
12
|
+
your app.
|
13
|
+
|
14
|
+
To use the shim with apps that read configuration from the filesystem, use the --dir
|
15
|
+
option to tell the shim where to rewrite configuration files. It will look for tokens
|
16
|
+
in JSON or YML that look like <<cmdb.key.name>> and replace them with the value of
|
17
|
+
the specified key.
|
18
|
+
|
19
|
+
To use the shim with 12-factor apps, use the --env option to tell the shim to load
|
20
|
+
every CMDB key into the environment. When using --env, the prefix of each key is
|
21
|
+
omitted from the environment variable name, e.g. "common.database.host" is
|
22
|
+
represented as DATABASE_HOST.
|
23
|
+
|
24
|
+
To support "development mode" and reload your app whenever its files change on disk,
|
25
|
+
use the --reload option and specify the name of a CMDB key that will enable this
|
26
|
+
behavior.
|
27
|
+
|
28
|
+
Usage:
|
29
|
+
cmdb shim [options] -- <command_to_exec> [options_for_command]
|
30
|
+
|
31
|
+
Where [options] are selected from:
|
32
|
+
EOS
|
33
|
+
opt :dir,
|
34
|
+
"Directory to scan for key-replacement tokens in data files",
|
35
|
+
:type => :string
|
36
|
+
opt :consul_url,
|
37
|
+
"The URL for talking to consul",
|
38
|
+
:type => :string
|
39
|
+
opt :consul_prefix,
|
40
|
+
"The prefix to use when getting keys from consul, can be specified more than once",
|
41
|
+
:type => :string,
|
42
|
+
:multi => true
|
43
|
+
opt :keys,
|
44
|
+
"Override search path(s) for CMDB key files",
|
45
|
+
:type => :strings
|
46
|
+
opt :pretend,
|
47
|
+
"Check for errors, but do not actually launch the app or rewrite files",
|
48
|
+
:default => false
|
49
|
+
opt :quiet,
|
50
|
+
"Don't print any output",
|
51
|
+
:default => false
|
52
|
+
opt :reload,
|
53
|
+
"CMDB key that enables reload-on-edit",
|
54
|
+
:type => :string
|
55
|
+
opt :reload_signal,
|
56
|
+
"Signal to send to app server when code is edited",
|
57
|
+
:type => :string,
|
58
|
+
:default => "HUP"
|
59
|
+
opt :env,
|
60
|
+
"Add CMDB keys to the app server's process environment",
|
61
|
+
:default => false
|
62
|
+
opt :user,
|
63
|
+
"Switch to named user before executing app",
|
64
|
+
:type => :string
|
65
|
+
opt :root,
|
66
|
+
"Promote named subkey to the root when it is present in a namespace",
|
67
|
+
:type => :string
|
68
|
+
end
|
69
|
+
|
70
|
+
self.new(ARGV, options)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Irrevocably change the current user for this Unix process by calling the
|
74
|
+
# setresuid system call. This sets both the uid and gid (to the user's primary
|
75
|
+
# group).
|
76
|
+
#
|
77
|
+
# @param [String] login name of user to switch to
|
78
|
+
# @return [true]
|
79
|
+
# @raise [ArgumentError] if the named user does not exist
|
80
|
+
def self.drop_privileges(login)
|
81
|
+
pwent = Etc.getpwnam(login)
|
82
|
+
Process::Sys.setresgid(pwent.gid, pwent.gid, pwent.gid)
|
83
|
+
Process::Sys.setresuid(pwent.uid, pwent.uid, pwent.uid)
|
84
|
+
true
|
85
|
+
end
|
86
|
+
|
87
|
+
# @return [CMDB::Interface]
|
88
|
+
attr_reader :cmdb
|
89
|
+
|
90
|
+
# Create a Shim.
|
91
|
+
# @param [Array] command collection of string to pass to Kernel#exec; 0th element is the command name
|
92
|
+
# @options [String] :condif_dir
|
93
|
+
def initialize(command, options={})
|
94
|
+
@command = command
|
95
|
+
@dir = options[:dir]
|
96
|
+
@consul_url = options[:consul_url]
|
97
|
+
@consul_prefixes = options[:consul_prefix]
|
98
|
+
@keys = options[:keys] || []
|
99
|
+
@pretend = options[:pretend]
|
100
|
+
@reload = options[:reload]
|
101
|
+
@signal = options[:reload_signal]
|
102
|
+
@env = options[:env]
|
103
|
+
@user = options[:user]
|
104
|
+
@root = options[:root]
|
105
|
+
|
106
|
+
unless @keys.empty?
|
107
|
+
CMDB::FileSource.base_directories = @keys
|
108
|
+
end
|
109
|
+
unless @consul_url.nil?
|
110
|
+
CMDB::ConsulSource.url = @consul_url
|
111
|
+
end
|
112
|
+
if !@consul_prefixes.nil? && !@consul_prefixes.empty?
|
113
|
+
CMDB::ConsulSource.prefixes = @consul_prefixes
|
114
|
+
end
|
115
|
+
|
116
|
+
if options[:quiet]
|
117
|
+
CMDB.log.level = Logger::FATAL
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Run the shim.
|
122
|
+
#
|
123
|
+
# @raise [SystemExit] if something goes wrong
|
124
|
+
def run
|
125
|
+
@cmdb = CMDB::Interface.new(:root=>@root)
|
126
|
+
|
127
|
+
rewrote = rewrite_files
|
128
|
+
populated = populate_environment
|
129
|
+
|
130
|
+
if (!rewrote && !populated && !@pretend && @command.empty?)
|
131
|
+
CMDB.log.warn "CMDB: nothing to do; please specify --dir, --env, or a command to run"
|
132
|
+
exit 7
|
133
|
+
end
|
134
|
+
|
135
|
+
launch_app
|
136
|
+
rescue CMDB::BadKey => e
|
137
|
+
CMDB.log.fatal "CMDB: Bad Key: malformed CMDB key '#{e.key}'"
|
138
|
+
exit 1
|
139
|
+
rescue CMDB::BadValue => e
|
140
|
+
CMDB.log.fatal "CMDB: Bad Value: illegal value for CMDB key '#{e.key}' in source #{e.url}"
|
141
|
+
exit 2
|
142
|
+
rescue CMDB::BadData => e
|
143
|
+
CMDB.log.fatal "CMDB: Bad Data: malformed CMDB data in source #{e.url}"
|
144
|
+
exit 3
|
145
|
+
rescue CMDB::ValueConflict => e
|
146
|
+
CMDB.log.fatal "CMDB: Value Conflict: #{e.message}"
|
147
|
+
e.sources.each do |s|
|
148
|
+
CMDB.log.fatal " - #{s.url}"
|
149
|
+
end
|
150
|
+
exit 4
|
151
|
+
rescue CMDB::NameConflict => e
|
152
|
+
CMDB.log.fatal "CMDB: Name Conflict: #{e.message}"
|
153
|
+
e.keys.each do |k|
|
154
|
+
CMDB.log.fatal " - #{k}"
|
155
|
+
end
|
156
|
+
exit 4
|
157
|
+
rescue CMDB::EnvironmentConflict => e
|
158
|
+
CMDB.log.fatal "CMDB: Environment Conflict: #{e.message}"
|
159
|
+
exit 5
|
160
|
+
rescue Errno::ENOENT => e
|
161
|
+
CMDB.log.fatal "CMDB: missing file or directory #{e.message}"
|
162
|
+
exit 6
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
# @return [Boolean]
|
168
|
+
def rewrite_files
|
169
|
+
return false unless @dir
|
170
|
+
|
171
|
+
CMDB.log.info 'Starting rewrite of configuration...'
|
172
|
+
|
173
|
+
rewriter = CMDB::Rewriter.new(@dir)
|
174
|
+
|
175
|
+
total = rewriter.rewrite(@cmdb)
|
176
|
+
|
177
|
+
if rewriter.missing_keys.any?
|
178
|
+
missing = rewriter.missing_keys.map { |k| " #{k}" }.join("\n")
|
179
|
+
CMDB.log.error "Cannot rewrite configuration; #{rewriter.missing_keys.size} missing keys:\n#{missing}"
|
180
|
+
|
181
|
+
exit -rewriter.missing_keys.size
|
182
|
+
end
|
183
|
+
|
184
|
+
report_rewrite(total)
|
185
|
+
|
186
|
+
if @pretend
|
187
|
+
false
|
188
|
+
else
|
189
|
+
rewriter.save
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# @return [Boolean]
|
194
|
+
def populate_environment
|
195
|
+
return false unless @env
|
196
|
+
|
197
|
+
env = @cmdb.to_h
|
198
|
+
|
199
|
+
env.keys.each do |k|
|
200
|
+
raise CMDB::EnvironmentConflict.new(k) if ENV.key?(k)
|
201
|
+
end
|
202
|
+
|
203
|
+
if @pretend
|
204
|
+
false
|
205
|
+
else
|
206
|
+
env.each_pair do |k, v|
|
207
|
+
ENV[k] = v
|
208
|
+
end
|
209
|
+
true
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def launch_app
|
214
|
+
if @command.any?
|
215
|
+
# log this now, so that it gets logged even if @pretend
|
216
|
+
CMDB.log.info "App will run as user #{@user}" if @user
|
217
|
+
|
218
|
+
if @reload && @cmdb.get(@reload)
|
219
|
+
CMDB.log.info "SIG#{@signal}-on-edit is enabled; fork app"
|
220
|
+
fork_and_watch_app unless @pretend
|
221
|
+
else
|
222
|
+
CMDB.log.info "SIG#{@signal}-on-edit is disabled; exec app"
|
223
|
+
exec_app unless @pretend
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
def exec_app
|
229
|
+
self.class.drop_privileges(@user) if @user
|
230
|
+
exec(*@command)
|
231
|
+
end
|
232
|
+
|
233
|
+
def fork_and_watch_app
|
234
|
+
# let the child share our stdio handles on purpose
|
235
|
+
pid = fork do
|
236
|
+
exec_app
|
237
|
+
end
|
238
|
+
|
239
|
+
CMDB.log.info("App (pid %d) has been forked; watching %s" % [pid, ::Dir.pwd])
|
240
|
+
|
241
|
+
t0 = Time.at(0)
|
242
|
+
|
243
|
+
listener = Listen.to(::Dir.pwd) do |modified, added, removed|
|
244
|
+
modified = modified.select { |fn| interesting?(fn) }
|
245
|
+
added = added.select { |fn| interesting?(fn) }
|
246
|
+
removed = removed.select { |fn| interesting?(fn) }
|
247
|
+
next if modified.empty? && added.empty? && removed.empty?
|
248
|
+
|
249
|
+
begin
|
250
|
+
dt = Time.now - t0
|
251
|
+
if dt > 15
|
252
|
+
Process.kill(@signal, pid)
|
253
|
+
CMDB.log.info "Sent SIG%s to app (pid %d) because (modified,created,deleted)=(%d,%d,%d)" %
|
254
|
+
[@signal, pid, modified.size, added.size, removed.size]
|
255
|
+
t0 = Time.now
|
256
|
+
else
|
257
|
+
CMDB.log.error "Skipped SIG%s to app (pid %d) due to timeout (%d)" %
|
258
|
+
[@signal, pid, dt]
|
259
|
+
end
|
260
|
+
rescue
|
261
|
+
CMDB.log.error "Skipped SIG%s to app (pid %d) due to %s" %
|
262
|
+
[@signal, pid, $!.to_s]
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
listener.start
|
267
|
+
|
268
|
+
wpid, wstatus = nil, nil
|
269
|
+
|
270
|
+
loop do
|
271
|
+
begin
|
272
|
+
wpid, wstatus = Process.wait2(-1, Process::WNOHANG)
|
273
|
+
if wpid == pid && wstatus.exited?
|
274
|
+
break
|
275
|
+
elsif wpid
|
276
|
+
CMDB.log.info("Descendant (pid %d) has waited with %s" % [wpid, wstatus.inspect])
|
277
|
+
end
|
278
|
+
rescue
|
279
|
+
CMDB.log.error "Skipped wait2 to app (pid %d) due to %s" %
|
280
|
+
[pid, $!.to_s]
|
281
|
+
end
|
282
|
+
sleep(1)
|
283
|
+
end
|
284
|
+
|
285
|
+
CMDB.log.info("App (pid %d) has exited with %s" % [wpid, wstatus.inspect])
|
286
|
+
listener.stop
|
287
|
+
exit(wstatus.exitstatus || 43)
|
288
|
+
end
|
289
|
+
|
290
|
+
def report_rewrite(total)
|
291
|
+
CMDB.log.info "Replaced #{total} variables in #{@dir}"
|
292
|
+
end
|
293
|
+
|
294
|
+
def interesting?(fn)
|
295
|
+
!File.basename(fn).start_with?('.')
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'diplomat'
|
2
|
+
|
3
|
+
module CMDB
|
4
|
+
class ConsulSource
|
5
|
+
# Regular expression to match array values
|
6
|
+
ARRAY_VALUE = /^\[(.*)\]$/
|
7
|
+
|
8
|
+
# The url to communicate with consul
|
9
|
+
@@url = nil
|
10
|
+
# The prefixes to use when getting keys from consul
|
11
|
+
@@prefixes = nil
|
12
|
+
|
13
|
+
def self.url=(url)
|
14
|
+
@@url = url
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.url
|
18
|
+
@@url
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.prefixes=(prefixes)
|
22
|
+
@@prefixes = prefixes
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.prefixes
|
26
|
+
@@prefixes
|
27
|
+
end
|
28
|
+
|
29
|
+
# Initialize the configuration for consul source
|
30
|
+
def initialize(prefix)
|
31
|
+
Diplomat.configure do |config|
|
32
|
+
config.url = @@url
|
33
|
+
end
|
34
|
+
@prefix = prefix
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get a single key from consul
|
38
|
+
def get(key)
|
39
|
+
value = Diplomat::Kv.get(dot_to_slash(key))
|
40
|
+
process_value(value)
|
41
|
+
rescue Diplomat::KeyNotFound
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
# Not implemented for consul source
|
46
|
+
def each_pair(&block)
|
47
|
+
prefix = @prefix || ''
|
48
|
+
all = Diplomat::Kv.get(prefix, recurse: true)
|
49
|
+
all.each do |item|
|
50
|
+
dotted_prefix = prefix.split('/').join('.')
|
51
|
+
dotted_key = item[:key].split('/').join('.')
|
52
|
+
key = dotted_prefix == '' ? dotted_key : dotted_key.split("#{dotted_prefix}.").last
|
53
|
+
value = process_value(item[:value])
|
54
|
+
puts "Key: #{key}, Value: #{value}"
|
55
|
+
block.call(dotted_key.split("#{dotted_prefix}.").last, process_value(item[:value]))
|
56
|
+
end
|
57
|
+
rescue Diplomat::KeyNotFound
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def process_value(val)
|
63
|
+
return JSON.load(val)
|
64
|
+
rescue Exception
|
65
|
+
return val
|
66
|
+
end
|
67
|
+
|
68
|
+
# Converts the dotted notation to a slashed notation. If a @@prefix is set, it applies the prefix.
|
69
|
+
# @example
|
70
|
+
# "common.proxy.endpoints" => common/proxy/endpoints (or) shard403/common/proxy/endpoints
|
71
|
+
def dot_to_slash(key)
|
72
|
+
key = "#{@prefix}.#{key}" unless @prefix.nil?
|
73
|
+
key.split('.').join('/')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module CMDB
|
4
|
+
# Data source that is backed by a YAML file that lives in the filesystem. The name of the YAML
|
5
|
+
# file becomes the top-level key under which all values in the YAML are exposed, preserving
|
6
|
+
# their exact structure as parsed by YAML.
|
7
|
+
#
|
8
|
+
# @example Use my.yml as a CMDB source
|
9
|
+
# source = FileSource.new('/tmp/my.yml') # contains a top-level stanza named "database"
|
10
|
+
# source['my']['database']['host'] # => 'db1-1.example.com'
|
11
|
+
class FileSource
|
12
|
+
# @return [URI] a file:// URL describing where this source's data comes from
|
13
|
+
attr_reader :prefix, :url
|
14
|
+
|
15
|
+
@@base_directories = ['/var/lib/cmdb', File.expand_path('~/.cmdb')]
|
16
|
+
|
17
|
+
# List of directories that will be searched (in order) for YML files at load time.
|
18
|
+
# @return [Array] collection of String
|
19
|
+
def self.base_directories
|
20
|
+
@@base_directories
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [Array] bd collection of String absolute paths to search for YML files
|
24
|
+
def self.base_directories=(bd)
|
25
|
+
@@base_directories = bd
|
26
|
+
end
|
27
|
+
|
28
|
+
# Construct a new FileSource from an input YML file.
|
29
|
+
# @param [String,Pathname] filename path to a YAML file
|
30
|
+
# @raise [BadData] if the file's content is malformed
|
31
|
+
def initialize(filename, root=nil)
|
32
|
+
filename = File.expand_path(filename)
|
33
|
+
@url = URI.parse("file://#{filename}")
|
34
|
+
@prefix = File.basename(filename, ".*")
|
35
|
+
@extension = File.extname(filename)
|
36
|
+
@data = {}
|
37
|
+
|
38
|
+
raw_bytes = File.read(filename)
|
39
|
+
raw_data = nil
|
40
|
+
|
41
|
+
begin
|
42
|
+
case @extension
|
43
|
+
when /jso?n?$/i
|
44
|
+
raw_data = JSON.load(raw_bytes)
|
45
|
+
when /ya?ml$/i
|
46
|
+
raw_data = YAML.load(raw_bytes)
|
47
|
+
else
|
48
|
+
raise BadData.new(self.url, 'file with unknown extension; expected js(on) or y(a)ml')
|
49
|
+
end
|
50
|
+
rescue Exception
|
51
|
+
raise BadData.new(self.url, 'CMDB data file')
|
52
|
+
end
|
53
|
+
|
54
|
+
raw_data = raw_data[root] if !root.nil? && raw_data.key?(root)
|
55
|
+
flatten(raw_data, @prefix, @data)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Get the value of key.
|
59
|
+
#
|
60
|
+
# @return [nil,String,Numeric,TrueClass,FalseClass,Array] the key's value, or nil if not found
|
61
|
+
def get(key)
|
62
|
+
@data[key]
|
63
|
+
end
|
64
|
+
|
65
|
+
# Enumerate the keys in this source, and their values.
|
66
|
+
#
|
67
|
+
# @yield every key/value in the source
|
68
|
+
# @yieldparam [String] key
|
69
|
+
# @yieldparam [Object] value
|
70
|
+
def each_pair(&block)
|
71
|
+
# Strip the prefix in the key and call the block
|
72
|
+
@data.each_pair { |k, v| block.call(k.split("#{@prefix}.").last, v) }
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def flatten(data, prefix, output)
|
78
|
+
data.each_pair do |key, value|
|
79
|
+
key = "#{prefix}.#{key}"
|
80
|
+
case value
|
81
|
+
when Hash
|
82
|
+
flatten(value, key, output)
|
83
|
+
when Array
|
84
|
+
if value.all? { |e| e.is_a?(String) } ||
|
85
|
+
value.all? { |e| e.is_a?(Numeric) } ||
|
86
|
+
value.all? { |e| e == true } ||
|
87
|
+
value.all? { |e| e == false }
|
88
|
+
output[key] = value
|
89
|
+
else
|
90
|
+
# mismatched arrays: not allowed
|
91
|
+
raise BadValue.new(self.url, key, value)
|
92
|
+
end
|
93
|
+
when String, Numeric, TrueClass, FalseClass
|
94
|
+
output[key] = value
|
95
|
+
else
|
96
|
+
# nil and anything else: not allowed
|
97
|
+
raise BadValue.new(self.url, key, value)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module CMDB
|
4
|
+
class Interface
|
5
|
+
# Create a new instance of the CMDB interface.
|
6
|
+
# @option settings [String] root name of subkey to consider as root
|
7
|
+
def initialize(settings={})
|
8
|
+
@root = settings[:root] if settings
|
9
|
+
|
10
|
+
namespaces = {}
|
11
|
+
|
12
|
+
load_file_sources(namespaces)
|
13
|
+
check_overlap(namespaces)
|
14
|
+
|
15
|
+
@sources = []
|
16
|
+
# Load from consul source first if one is available.
|
17
|
+
if !ConsulSource.url.nil?
|
18
|
+
if ConsulSource.prefixes.nil? || ConsulSource.prefixes.empty?
|
19
|
+
@sources << ConsulSource.new('')
|
20
|
+
else
|
21
|
+
ConsulSource.prefixes.each do |prefix|
|
22
|
+
@sources << ConsulSource.new(prefix)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
# Register valid sources with CMDB
|
27
|
+
namespaces.each do |_, v|
|
28
|
+
@sources << v.first
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retrieve the value of a CMDB key, searching all sources in the order they were initialized.
|
33
|
+
#
|
34
|
+
# @return [Object,nil] the value of the key, or nil if key not found
|
35
|
+
# @param [String] key
|
36
|
+
# @raise [BadKey] if the key name is malformed
|
37
|
+
def get(key)
|
38
|
+
raise BadKey.new(key) unless key =~ VALID_KEY
|
39
|
+
value = nil
|
40
|
+
|
41
|
+
@sources.each do |s|
|
42
|
+
value = s.get(key)
|
43
|
+
break unless value.nil?
|
44
|
+
end
|
45
|
+
|
46
|
+
value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Retrieve the value of a CMDB key; raise an exception if the key is not found.
|
50
|
+
#
|
51
|
+
# @return [Object,nil] the value of the key
|
52
|
+
# @param [String] key
|
53
|
+
# @raise [MissingKey] if the key is absent from the CMDB
|
54
|
+
# @raise [BadKey] if the key name is malformed
|
55
|
+
def get!(key)
|
56
|
+
get(key) || raise(MissingKey.new(key))
|
57
|
+
end
|
58
|
+
|
59
|
+
# Enumerate all of the keys in the CMDB.
|
60
|
+
#
|
61
|
+
# @yield every key/value in the CMDB
|
62
|
+
# @yieldparam [String] key
|
63
|
+
# @yieldparam [Object] value
|
64
|
+
# @return [Interface] always returns self
|
65
|
+
def each_pair(&block)
|
66
|
+
@sources.each do |s|
|
67
|
+
s.each_pair(&block)
|
68
|
+
end
|
69
|
+
|
70
|
+
self
|
71
|
+
end
|
72
|
+
|
73
|
+
# Transform the entire CMDB into a flat Hash that can be merged into ENV.
|
74
|
+
# Key names are transformed into underscore-separated, uppercase strings;
|
75
|
+
# all runs of non-alphanumeric, non-underscore characters are tranformed
|
76
|
+
# into a single underscore.
|
77
|
+
#
|
78
|
+
# The transformation rules make it possible for key names to conflict,
|
79
|
+
# e.g. "apple.orange.pear" and "apple.orange_pear" cannot exist in
|
80
|
+
# the same flat hash. This method checks for such conflicts and raises
|
81
|
+
# rather than returning bad data.
|
82
|
+
#
|
83
|
+
# @raise [NameConflict] if two or more key names transform to the same
|
84
|
+
def to_h
|
85
|
+
values = {}
|
86
|
+
sources = {}
|
87
|
+
|
88
|
+
each_pair do |key, value|
|
89
|
+
env_key = key_to_env(key)
|
90
|
+
value = JSON.dump(value) unless value.is_a?(String)
|
91
|
+
|
92
|
+
if sources.key?(env_key)
|
93
|
+
raise NameConflict.new(env_key, [sources[env_key], key])
|
94
|
+
else
|
95
|
+
sources[env_key] = key
|
96
|
+
values[env_key] = value_to_env(value)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
values
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# Scan for CMDB data files and index them by namespace
|
106
|
+
def load_file_sources(namespaces)
|
107
|
+
# Consult standard base directories for data files
|
108
|
+
directories = FileSource.base_directories
|
109
|
+
|
110
|
+
# Also consult working dir in development environments
|
111
|
+
if CMDB.development?
|
112
|
+
local_dir = File.join(Dir.pwd, '.cmdb')
|
113
|
+
directories += [local_dir]
|
114
|
+
end
|
115
|
+
|
116
|
+
directories.each do |dir|
|
117
|
+
(Dir.glob(File.join(dir, '*.js')) + Dir.glob(File.join(dir, '*.json'))).each do |filename|
|
118
|
+
source = FileSource.new(filename, @root)
|
119
|
+
namespaces[source.prefix] ||= []
|
120
|
+
namespaces[source.prefix] << source
|
121
|
+
end
|
122
|
+
|
123
|
+
(Dir.glob(File.join(dir, '*.yml')) + Dir.glob(File.join(dir, '*.yaml'))).each do |filename|
|
124
|
+
source = FileSource.new(filename, @root)
|
125
|
+
namespaces[source.prefix] ||= []
|
126
|
+
namespaces[source.prefix] << source
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Check for overlapping namespaces and react appropriately. This can happen when a file
|
132
|
+
# of a given name is located in more than one of the key-search directories. We tolerate
|
133
|
+
# this in development mode, but raise an exception otherwise.
|
134
|
+
def check_overlap(namespaces)
|
135
|
+
overlapping = namespaces.select { |_, sources| sources.size > 1 }
|
136
|
+
overlapping.each do |ns, sources|
|
137
|
+
exc = ValueConflict.new(ns, sources)
|
138
|
+
|
139
|
+
if CMDB.development?
|
140
|
+
CMDB.log.warn exc.message
|
141
|
+
else
|
142
|
+
raise exc
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Make an environment variable out of a key name
|
148
|
+
def key_to_env(key)
|
149
|
+
env_name = key
|
150
|
+
env_name.gsub!(/[^A-Za-z0-9_]+/,'_')
|
151
|
+
env_name.upcase!
|
152
|
+
env_name
|
153
|
+
end
|
154
|
+
|
155
|
+
# Make a CMDB value storable in the process environment (ENV hash)
|
156
|
+
def value_to_env(value)
|
157
|
+
case value
|
158
|
+
when String
|
159
|
+
value
|
160
|
+
else
|
161
|
+
JSON.dump(value)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module CMDB
|
5
|
+
# Tool that visits every file in a hierarchy and rewrites its references to CMDB inputs.
|
6
|
+
#
|
7
|
+
# References look like <<name.of.key>> and may be replaced with a scalar value
|
8
|
+
# or an array depending on the type of the input value.
|
9
|
+
class Rewriter
|
10
|
+
attr_reader :missing_keys
|
11
|
+
|
12
|
+
# Create a new shim to rewrite config files in a chosen dir and all subdirs.
|
13
|
+
# config_dir is transformed into an absolute path (if it isn't already)
|
14
|
+
# before any rewrite operations occur.
|
15
|
+
#
|
16
|
+
# @param [String,Pathname] config_dir
|
17
|
+
def initialize(config_dir)
|
18
|
+
@dir = File.expand_path(config_dir)
|
19
|
+
@rewriters = []
|
20
|
+
end
|
21
|
+
|
22
|
+
# Substitute CMDB input values into config files whenever a replacement token is encountered.
|
23
|
+
#
|
24
|
+
# @param [CMDB::Interface] cmdb
|
25
|
+
# @return [Integer] the number of variables replaced
|
26
|
+
def rewrite(cmdb)
|
27
|
+
raise Errno::ENOENT.new(@dir) unless File.directory?(@dir)
|
28
|
+
|
29
|
+
visit(@dir)
|
30
|
+
total = 0
|
31
|
+
@rewriters.each { |rw| total += rw.rewrite(cmdb) }
|
32
|
+
|
33
|
+
@missing_keys = @rewriters.map { |rw| rw.missing_keys}.flatten.uniq.sort
|
34
|
+
|
35
|
+
total
|
36
|
+
end
|
37
|
+
|
38
|
+
def save
|
39
|
+
@rewriters.each { |rw| rw.save }
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# Recursively scan location for files to rewrite.
|
46
|
+
def visit(location)
|
47
|
+
if File.file?(location)
|
48
|
+
scan(location)
|
49
|
+
elsif File.directory?(location)
|
50
|
+
entries = Dir.glob("#{location}/*")
|
51
|
+
subdirs = entries.select { |e| File.directory?(e) }
|
52
|
+
files = entries.select { |e| File.file?(e) }
|
53
|
+
|
54
|
+
subdirs.each do |entry|
|
55
|
+
visit(entry)
|
56
|
+
end
|
57
|
+
|
58
|
+
files.each do |entry|
|
59
|
+
visit(entry)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Load a data file and attach a rewriter to it.
|
65
|
+
def scan(file)
|
66
|
+
case File.extname(file)
|
67
|
+
when '.yml', '.yaml'
|
68
|
+
@rewriters << FileRewriter.new(file, YAML)
|
69
|
+
when '.js', '.json'
|
70
|
+
@rewriters << FileRewriter.new(file, JSON)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Tool that rewrites the contents of a single YAML, JSON or similar data file.
|
76
|
+
# The rewriting is done in-memory and isn't saved back to disk until someone
|
77
|
+
# calls #save, allowing the caller to check #missing_keys before making a
|
78
|
+
# decision whether to proceed.
|
79
|
+
class FileRewriter
|
80
|
+
# Regexp that matches a well-formed replacement token in YML or JSON
|
81
|
+
REPLACEMENT_TOKEN = /^<<(.*)>>$/
|
82
|
+
|
83
|
+
attr_reader :missing_keys
|
84
|
+
|
85
|
+
# Load YAML, JSON or similar into memory as a Ruby object graph.
|
86
|
+
def initialize(file, encoder)
|
87
|
+
@file = file
|
88
|
+
@encoder = encoder
|
89
|
+
@data = @encoder.load(File.read(file))
|
90
|
+
end
|
91
|
+
|
92
|
+
# Peform CMDB input replacement on in-memory objects. Validate that the result can be saved.
|
93
|
+
#
|
94
|
+
# @return [Integer] number of variables replaced
|
95
|
+
def rewrite(cmdb)
|
96
|
+
@total = 0
|
97
|
+
@missing_keys = []
|
98
|
+
@data = visit(cmdb, @data)
|
99
|
+
|
100
|
+
# Very important; DO NOT REMOVE. This is how we validate that #save will work.
|
101
|
+
@encoder.dump(@data)
|
102
|
+
raise Errno::EACCES.new(@file) unless File.writable?(@file)
|
103
|
+
|
104
|
+
@total
|
105
|
+
end
|
106
|
+
|
107
|
+
def save
|
108
|
+
File.open(@file, 'w') { |f| f.write @encoder.dump(@data) }
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Recurse through an object graph, finding replacement tokens and substituting the
|
114
|
+
# corresponding CMDB values.
|
115
|
+
def visit(cmdb, node)
|
116
|
+
if node.is_a?(Hash)
|
117
|
+
result = {}
|
118
|
+
node.each_pair do |k, v|
|
119
|
+
result[k] = visit(cmdb, v)
|
120
|
+
end
|
121
|
+
elsif node.is_a?(Array)
|
122
|
+
result = []
|
123
|
+
node.each do |v|
|
124
|
+
result << visit(cmdb, v)
|
125
|
+
end
|
126
|
+
elsif node.is_a?(String) && (m = REPLACEMENT_TOKEN.match(node))
|
127
|
+
value = cmdb.get(m[1])
|
128
|
+
if value.nil?
|
129
|
+
@missing_keys << m[1]
|
130
|
+
else
|
131
|
+
result = value
|
132
|
+
@total += 1
|
133
|
+
end
|
134
|
+
else
|
135
|
+
result = node
|
136
|
+
end
|
137
|
+
|
138
|
+
result
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/lib/cmdb/version.rb
ADDED
data/lib/cmdb.rb
CHANGED
@@ -1,2 +1,140 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'singleton'
|
3
|
+
|
1
4
|
module CMDB
|
5
|
+
# Values of RACK_ENV/RAILS_ENV that are considered to be "development," which relaxes
|
6
|
+
# certain runtime sanity checks.
|
7
|
+
DEVELOPMENT_ENVIRONMENTS = [nil, 'development', 'test'].freeze
|
8
|
+
|
9
|
+
# Regexp that matches valid key names. Key names consist of one or more dot-separated words;
|
10
|
+
# each word must begin with a lowercase alpha character and may contain alphanumerics or
|
11
|
+
# underscores.
|
12
|
+
VALID_KEY = /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)*$/
|
13
|
+
|
14
|
+
class Error < StandardError; end
|
15
|
+
|
16
|
+
# Client asserted the existence of a key that does not exist in the CMDB.
|
17
|
+
class MissingKey < Error
|
18
|
+
# @return [String] the name of the offending key
|
19
|
+
attr_reader :key
|
20
|
+
|
21
|
+
# @param [String] name
|
22
|
+
def initialize(key)
|
23
|
+
@key = key
|
24
|
+
super("Key '#{key}' not found in CMDB")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Client asked for an invalid or malformed key name.
|
29
|
+
class BadKey < Error
|
30
|
+
# @return [String] the name of the offending key
|
31
|
+
attr_reader :key
|
32
|
+
|
33
|
+
# @param [String] name
|
34
|
+
def initialize(key)
|
35
|
+
@key = key
|
36
|
+
super("Malformed key '#{key}'")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# A value of an unsupported type was encountered in the CMDB.
|
41
|
+
class BadValue < Error
|
42
|
+
# @return [URI] filesystem or network location of the bad value
|
43
|
+
attr_reader :url
|
44
|
+
|
45
|
+
# @return [String] the name of the key that contained the bad value
|
46
|
+
attr_reader :key
|
47
|
+
|
48
|
+
# @param [URI] url filesystem or network location of the bad value
|
49
|
+
# @param [String] key CMDB key name under which the bad value was found
|
50
|
+
# @param [Object] value the bad value itself
|
51
|
+
def initialize(url, key, value)
|
52
|
+
@url = url
|
53
|
+
@key = key
|
54
|
+
super("Values of type #{value.class.name} are unsupported")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Malformed data was encountered in the CMDB or in an app's filesystem.
|
59
|
+
class BadData < Error
|
60
|
+
# @return [URI] filesystem or network location of the bad data
|
61
|
+
attr_reader :url
|
62
|
+
|
63
|
+
# @param [URI] url filesystem or network location of the bad value
|
64
|
+
# @param [String] context brief description of where data was found e.g. 'CMDB data file' or 'input config file'
|
65
|
+
def initialize(url, context=nil)
|
66
|
+
@url = url
|
67
|
+
super("Malformed data encountered #{(' in ' + context) if context}")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Two or more sources contain keys for the same namespace; this is only allowed in development
|
72
|
+
# environments.
|
73
|
+
class ValueConflict < Error
|
74
|
+
attr_reader :sources
|
75
|
+
|
76
|
+
def initialize(ns, sources)
|
77
|
+
@sources = sources
|
78
|
+
super("Keys for namespace #{ns} are defined in #{sources.size} overlapping sources")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Deprecated name for ValueConflict
|
83
|
+
Conflict = ValueConflict
|
84
|
+
|
85
|
+
# Two or more keys in different namespaces have an identical name. This isn't an error
|
86
|
+
# when CMDB is used to refer to keys by their full, qualified name, but it can become
|
87
|
+
# an issue when loading keys into the environment for 12-factor apps to process.
|
88
|
+
class NameConflict < Error
|
89
|
+
attr_reader :env
|
90
|
+
attr_reader :keys
|
91
|
+
|
92
|
+
def initialize(env, keys)
|
93
|
+
@env = env
|
94
|
+
@keys = keys
|
95
|
+
super("#{env} corresponds to #{keys.size} different keys")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# The CMDB is being exported to ENV, but one of its keys would overwrite a value that
|
100
|
+
# is already present in ENV. This should never happen, because the CMDB is designed to
|
101
|
+
# _augment_ the environment by providing a place to store boring (static, non-secret)
|
102
|
+
# inputs, and a given input should be set either in the CMDB or the environment, never
|
103
|
+
# both.
|
104
|
+
#
|
105
|
+
class EnvironmentConflict < Error
|
106
|
+
attr_reader :key
|
107
|
+
|
108
|
+
def initialize(key)
|
109
|
+
@key = key
|
110
|
+
super("#{key} is already present in the environment; cannot override with CMDB values")
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
module_function
|
115
|
+
|
116
|
+
def log
|
117
|
+
unless @log
|
118
|
+
@log = Logger.new(STDOUT)
|
119
|
+
@log.level = Logger::WARN
|
120
|
+
end
|
121
|
+
|
122
|
+
@log
|
123
|
+
end
|
124
|
+
|
125
|
+
def log=(log)
|
126
|
+
@log = log
|
127
|
+
end
|
128
|
+
|
129
|
+
# Determine whether CMDB is running in a development environment.
|
130
|
+
# @return [Boolean]
|
131
|
+
def development?
|
132
|
+
DEVELOPMENT_ENVIRONMENTS.include?(ENV['RACK_ENV'] || ENV['RAILS_ENV'])
|
133
|
+
end
|
2
134
|
end
|
135
|
+
|
136
|
+
require 'cmdb/consul_source'
|
137
|
+
require 'cmdb/file_source'
|
138
|
+
require 'cmdb/interface'
|
139
|
+
require 'cmdb/rewriter'
|
140
|
+
require 'cmdb/commands'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: cmdb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- RightScale
|
8
8
|
autorequire:
|
9
|
-
bindir:
|
9
|
+
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-02-
|
11
|
+
date: 2016-02-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: listen
|
@@ -69,7 +69,8 @@ dependencies:
|
|
69
69
|
description: Reads CMDB variables from files, Consul, and elsewhere.
|
70
70
|
email:
|
71
71
|
- rubygems@rightscale.com
|
72
|
-
executables:
|
72
|
+
executables:
|
73
|
+
- cmdb
|
73
74
|
extensions: []
|
74
75
|
extra_rdoc_files: []
|
75
76
|
files:
|
@@ -85,11 +86,20 @@ files:
|
|
85
86
|
- bin/console
|
86
87
|
- bin/setup
|
87
88
|
- cmdb.gemspec
|
89
|
+
- exe/cmdb
|
88
90
|
- fixtures/Gemfile
|
89
91
|
- fixtures/Gemfile.lock
|
90
92
|
- fixtures/app/widgets.rb
|
91
93
|
- fixtures/config.ru
|
92
94
|
- lib/cmdb.rb
|
95
|
+
- lib/cmdb/commands.rb
|
96
|
+
- lib/cmdb/commands/help.rb
|
97
|
+
- lib/cmdb/commands/shim.rb
|
98
|
+
- lib/cmdb/consul_source.rb
|
99
|
+
- lib/cmdb/file_source.rb
|
100
|
+
- lib/cmdb/interface.rb
|
101
|
+
- lib/cmdb/rewriter.rb
|
102
|
+
- lib/cmdb/version.rb
|
93
103
|
homepage: https://github.com/rightscale/cmdb
|
94
104
|
licenses:
|
95
105
|
- MIT
|