cmdb 0.1.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|