qb 0.3.25 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/ansible.cfg +10 -1
- data/exe/.qb_interop_receive +3 -10
- data/exe/qb +8 -2
- data/lib/python/qb/__init__.py +6 -0
- data/{roles/qb/ruby/rspec/setup/tasks/persistence.yml → lib/python/qb/ansible/__init__.py} +0 -0
- data/lib/python/qb/ansible/modules/__init__.py +0 -0
- data/lib/python/qb/ansible/modules/docker/__init__.py +0 -0
- data/lib/python/qb/ansible/modules/docker/client.py +177 -0
- data/lib/python/qb/ansible/modules/docker/image_manager.py +754 -0
- data/lib/python/qb/ipc/__init__.py +0 -0
- data/lib/python/qb/ipc/stdio/__init__.py +99 -0
- data/lib/python/qb/ipc/stdio/logging.py +151 -0
- data/lib/qb.rb +3 -3
- data/lib/qb/ansible/cmds/playbook.rb +5 -14
- data/lib/qb/ansible/env.rb +36 -6
- data/lib/qb/ansible/module.rb +396 -152
- data/lib/qb/ansible/module/response.rb +195 -0
- data/lib/qb/ansible/modules.rb +42 -0
- data/lib/qb/ansible/modules/docker/image.rb +273 -0
- data/lib/qb/cli.rb +5 -18
- data/lib/qb/cli/run.rb +2 -2
- data/lib/qb/data.rb +22 -0
- data/lib/qb/data/immutable.rb +39 -0
- data/lib/qb/docker.rb +2 -0
- data/lib/qb/docker/cli.rb +430 -0
- data/lib/qb/docker/image.rb +207 -0
- data/lib/qb/docker/image/name.rb +309 -0
- data/lib/qb/docker/image/tag.rb +113 -0
- data/lib/qb/docker/repo.rb +0 -0
- data/lib/qb/errors.rb +17 -3
- data/lib/qb/execution.rb +83 -0
- data/lib/qb/ipc.rb +48 -0
- data/lib/qb/ipc/stdio.rb +32 -0
- data/lib/qb/ipc/stdio/client.rb +267 -0
- data/lib/qb/ipc/stdio/server.rb +229 -0
- data/lib/qb/ipc/stdio/server/in_service.rb +18 -0
- data/lib/qb/ipc/stdio/server/log_service.rb +168 -0
- data/lib/qb/ipc/stdio/server/out_service.rb +20 -0
- data/lib/qb/ipc/stdio/server/service.rb +229 -0
- data/lib/qb/options.rb +360 -502
- data/lib/qb/options/option.rb +293 -115
- data/lib/qb/options/option/option_parser_concern.rb +228 -0
- data/lib/qb/options/types.rb +73 -0
- data/lib/qb/package.rb +0 -1
- data/lib/qb/package/version.rb +179 -58
- data/lib/qb/package/version/from.rb +192 -51
- data/lib/qb/package/version/leveled.rb +1 -1
- data/lib/qb/path.rb +3 -2
- data/lib/qb/repo/git.rb +9 -85
- data/lib/qb/role/default_dir.rb +2 -2
- data/lib/qb/role/errors.rb +2 -8
- data/lib/qb/util.rb +1 -2
- data/lib/qb/util/bundler.rb +73 -43
- data/lib/qb/util/decorators.rb +99 -0
- data/lib/qb/util/interop.rb +7 -8
- data/lib/qb/util/resource.rb +12 -13
- data/lib/qb/version.rb +10 -0
- data/library/path_facts +5 -10
- data/library/qb.module.rb +105 -0
- data/library/stream +6 -26
- data/load/ansible/module/autorun.rb +25 -0
- data/load/ansible/module/script.rb +123 -0
- data/load/rebundle.rb +39 -0
- data/plugins/filter/dict_filters.py +56 -0
- data/plugins/{filter_plugins/path_plugins.py → filter/path_filters.py} +0 -0
- data/plugins/{filter_plugins/ruby_interop_plugins.py → filter/ruby_interop_filters.py} +1 -17
- data/plugins/{filter_plugins/string_plugins.py → filter/string_filters.py} +1 -20
- data/plugins/{filter_plugins/version_plugins.py → filter/version_filters.py} +3 -18
- data/plugins/{lookup_plugins/every.py → lookup/every_lookups.py} +0 -0
- data/plugins/{lookup_plugins/resolve.py → lookup/resolve_lookups.py} +0 -0
- data/plugins/{lookup_plugins/version.py → lookup/version_lookups.py} +0 -16
- data/plugins/test/dict_tests.py +36 -0
- data/plugins/test/string_tests.py +36 -0
- data/qb.gemspec +7 -3
- data/roles/nrser.rb/library/set_fact_with_ruby.rb +3 -9
- data/roles/nrser.state_mate/library/state +3 -17
- data/roles/qb/call/meta/qb.yml +1 -1
- data/roles/qb/dev/ref/repo/git/meta/qb.yml +1 -1
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/defaults/main.yml +1 -1
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/main.yml +3 -2
- data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/qb.yml +12 -7
- data/roles/qb/docker/mac/kubernetes/tasks/main.yml +45 -0
- data/roles/qb/git/check/clean/meta/qb.yml +1 -1
- data/roles/qb/git/ignore/meta/qb +10 -3
- data/roles/qb/git/submodule/update/library/git_submodule_update +17 -27
- data/roles/qb/github/pages/setup/meta/qb.yml +1 -1
- data/roles/qb/labs/atom/apm/meta/qb.yml +1 -1
- data/roles/qb/osx/git/change_case/meta/qb.yml +1 -1
- data/roles/qb/osx/notif/meta/qb.yml +1 -1
- data/roles/qb/pkg/bump/library/bump +4 -16
- data/roles/qb/role/qb/defaults/main.yml +2 -0
- data/roles/qb/role/qb/meta/qb.yml +10 -5
- data/roles/qb/role/qb/templates/qb.yml.j2 +7 -2
- data/roles/qb/role/templates/library/module.rb.j2 +12 -23
- data/roles/qb/role/templates/meta/main.yml.j2 +14 -1
- data/roles/qb/ruby/bundler/meta/qb.yml +1 -1
- data/roles/qb/ruby/dependency/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/bin_stubs/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/bin_stubs/templates/console +8 -2
- data/roles/qb/ruby/gem/build/meta/qb.yml +1 -1
- data/roles/qb/ruby/gem/new/meta/qb.yml +1 -1
- data/roles/qb/ruby/nrser/rspex/generate/meta/qb.yml +5 -5
- data/roles/qb/ruby/nrser/rspex/issue/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/clean/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/config/library/yard.get_output_dir +5 -15
- data/roles/qb/ruby/yard/config/meta/qb.yml +1 -1
- data/roles/qb/ruby/yard/setup/meta/qb.yml +1 -1
- metadata +71 -22
- data/lib/qb/ansible_module.rb +0 -5
- data/lib/qb/util/stdio.rb +0 -187
- data/roles/qb/ruby/rspec/setup/tasks/main.yml +0 -4
File without changes
|
@@ -0,0 +1,99 @@
|
|
1
|
+
from __future__ import absolute_import, division, print_function
|
2
|
+
__metaclass__ = type
|
3
|
+
|
4
|
+
import os
|
5
|
+
import socket
|
6
|
+
|
7
|
+
def path_env_var_name(name):
|
8
|
+
return "QB_STDIO_{}".format(name.upper())
|
9
|
+
|
10
|
+
|
11
|
+
class Connection:
|
12
|
+
'''
|
13
|
+
Port of Ruby `QB::IPC::STDIO::Client::Connection` class.
|
14
|
+
'''
|
15
|
+
|
16
|
+
def __init__(self, name, type):
|
17
|
+
self.name = name
|
18
|
+
self.type = type
|
19
|
+
self.path = None
|
20
|
+
self.socket = None
|
21
|
+
self.env_var_name = path_env_var_name(self.name)
|
22
|
+
self.connected = False
|
23
|
+
|
24
|
+
def __str__(self):
|
25
|
+
attrs = ' '.join(
|
26
|
+
"{}={}".format(name, getattr(self, name))
|
27
|
+
for name in ('name', 'type', 'path', 'connected')
|
28
|
+
)
|
29
|
+
return "<qb.ipc.stdio.Connection {}>".format(attrs)
|
30
|
+
|
31
|
+
def get_path(self):
|
32
|
+
if self.env_var_name in os.environ:
|
33
|
+
self.path = os.environ[self.env_var_name]
|
34
|
+
return self.path
|
35
|
+
|
36
|
+
def connect(self, warnings=None):
|
37
|
+
if self.connected:
|
38
|
+
raise RuntimeError("{} is already connected!".format(self))
|
39
|
+
|
40
|
+
if self.get_path() is None:
|
41
|
+
return False
|
42
|
+
|
43
|
+
self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
44
|
+
|
45
|
+
try:
|
46
|
+
self.socket.connect(self.path)
|
47
|
+
except socket.error, msg:
|
48
|
+
if warngings is not None:
|
49
|
+
warning = 'Failed to connect to QB STDOUT stream at {}: {}'
|
50
|
+
warning = warning.format(qb_stdout_path, msg)
|
51
|
+
warnings.append(warning)
|
52
|
+
|
53
|
+
self.socket = None
|
54
|
+
return False
|
55
|
+
|
56
|
+
self.connected = True
|
57
|
+
|
58
|
+
return True
|
59
|
+
|
60
|
+
def disconnect(self):
|
61
|
+
if not self.connected:
|
62
|
+
raise RuntimeError("{} is not connected!".format(self))
|
63
|
+
|
64
|
+
# if self.type == 'out':
|
65
|
+
# self.socket.flush()
|
66
|
+
|
67
|
+
self.socket.close()
|
68
|
+
self.socket = None
|
69
|
+
self.connected = False
|
70
|
+
|
71
|
+
def println(self, line):
|
72
|
+
if not line.endswith( u"\n" ):
|
73
|
+
line = line + u"\n"
|
74
|
+
self.socket.sendall(line.encode("utf-8"))
|
75
|
+
|
76
|
+
|
77
|
+
class Client:
|
78
|
+
def __init__(self):
|
79
|
+
# I don't think need STDIN or we want to deal with what it means here
|
80
|
+
# self.stdin = Connection(name='in', type='in')
|
81
|
+
self.stdout = Connection(name='out', type='out')
|
82
|
+
self.stderr = Connection(name='err', type='out')
|
83
|
+
self.log = Connection(name='log', type='out')
|
84
|
+
|
85
|
+
def connections(self):
|
86
|
+
return [self.stdout, self.stderr, self.log]
|
87
|
+
|
88
|
+
def connect(self, warnings=None):
|
89
|
+
for connection in self.connections():
|
90
|
+
if not connection.connected:
|
91
|
+
connection.connect(warnings)
|
92
|
+
return self
|
93
|
+
|
94
|
+
def disconnect(sefl):
|
95
|
+
for connection in self.connections():
|
96
|
+
if connection.connected:
|
97
|
+
connection.disconnect()
|
98
|
+
|
99
|
+
client = Client()
|
@@ -0,0 +1,151 @@
|
|
1
|
+
from __future__ import absolute_import, division, print_function
|
2
|
+
__metaclass__ = type
|
3
|
+
|
4
|
+
import logging
|
5
|
+
import threading
|
6
|
+
import json
|
7
|
+
|
8
|
+
import qb.ipc.stdio
|
9
|
+
|
10
|
+
|
11
|
+
def getLogger(name, level=logging.DEBUG, io_client=qb.ipc.stdio.client):
|
12
|
+
logger = logging.getLogger(name)
|
13
|
+
if level is not None:
|
14
|
+
logger.setLevel(level)
|
15
|
+
logger.addHandler(Handler(io_client=io_client))
|
16
|
+
return Adapter(logger, {})
|
17
|
+
|
18
|
+
|
19
|
+
class Adapter(logging.LoggerAdapter):
|
20
|
+
def process(self, msg, kwds):
|
21
|
+
payload = None
|
22
|
+
if 'payload' in kwds:
|
23
|
+
payload = kwds['payload']
|
24
|
+
del kwds['payload']
|
25
|
+
|
26
|
+
if payload:
|
27
|
+
try:
|
28
|
+
msg = msg.format(**payload)
|
29
|
+
except:
|
30
|
+
pass
|
31
|
+
|
32
|
+
if 'extra' not in kwds:
|
33
|
+
kwds['extra'] = {}
|
34
|
+
|
35
|
+
kwds['extra']['payload'] = payload
|
36
|
+
|
37
|
+
return msg, kwds
|
38
|
+
|
39
|
+
|
40
|
+
class Handler(logging.Handler):
|
41
|
+
"""
|
42
|
+
A handler class which writes logging records to the QB master process
|
43
|
+
via it's `QB::IPC::STDIO` system, if available.
|
44
|
+
|
45
|
+
If QB's STDIO system is not available, discards the logs.
|
46
|
+
|
47
|
+
Based on the Python stdlib's `SocketHandler`, though it ended up retaining
|
48
|
+
almost nothing from it since it just proxies to
|
49
|
+
:class:`qb.ipc.stdio.Client`, which does all the socket dirty-work.
|
50
|
+
|
51
|
+
.. note:
|
52
|
+
This class **does not** connect the :class:`qb.ipc.stdio.Client`
|
53
|
+
instance (which defaults to the 'global' :attr:`qb.ipc.stdio.client`
|
54
|
+
instance - and that's what you should use unless you're testing or
|
55
|
+
doing something weird).
|
56
|
+
|
57
|
+
You need to connect the client somewhere else (before or after creating
|
58
|
+
loggers is fine).
|
59
|
+
|
60
|
+
"""
|
61
|
+
|
62
|
+
|
63
|
+
def __init__(self, io_client=qb.ipc.stdio.client):
|
64
|
+
"""
|
65
|
+
Initializes the handler with a :class:`qb.ipc.stdio.Client`, which
|
66
|
+
default to the 'global' one at :attr:`qb.ipc.stdio.client`. This should
|
67
|
+
be fine for everything except testing.
|
68
|
+
|
69
|
+
See note in class doc about connecting the client.
|
70
|
+
|
71
|
+
:param io_client: :class:`qb.ipc.stdio.Client`
|
72
|
+
"""
|
73
|
+
|
74
|
+
logging.Handler.__init__(self)
|
75
|
+
self.io_client = io_client
|
76
|
+
|
77
|
+
|
78
|
+
def send(self, string):
|
79
|
+
"""
|
80
|
+
Send a string to the :attr:`io_client`.
|
81
|
+
"""
|
82
|
+
|
83
|
+
if not self.io_client.log.connected:
|
84
|
+
return
|
85
|
+
|
86
|
+
self.io_client.log.println(string)
|
87
|
+
|
88
|
+
|
89
|
+
def get_sem_log_level(self, level):
|
90
|
+
"""
|
91
|
+
Trade Python log level string for a Ruby SemnaticLogger one.
|
92
|
+
"""
|
93
|
+
if level == 'DEBUG' or level == 'INFO' or level == 'ERROR':
|
94
|
+
return level.lower()
|
95
|
+
elif level == 'WARNING':
|
96
|
+
return 'warn'
|
97
|
+
elif level == 'CRITICAL':
|
98
|
+
return 'fatal'
|
99
|
+
else:
|
100
|
+
return 'info'
|
101
|
+
|
102
|
+
|
103
|
+
def emit(self, record):
|
104
|
+
"""
|
105
|
+
Emit a record.
|
106
|
+
Pickles the record and writes it to the socket in binary format.
|
107
|
+
If there is an error with the socket, silently drop the packet.
|
108
|
+
If there was a problem with the socket, re-establishes the
|
109
|
+
socket.
|
110
|
+
|
111
|
+
record: https://docs.python.org/2/library/logging.html#logrecord-attributes
|
112
|
+
"""
|
113
|
+
|
114
|
+
try:
|
115
|
+
self.format(record)
|
116
|
+
|
117
|
+
struct = dict(
|
118
|
+
level = self.get_sem_log_level(record.levelname),
|
119
|
+
name = record.name,
|
120
|
+
pid = record.process,
|
121
|
+
# thread = threading.current_thread().name,
|
122
|
+
thread = record.threadName,
|
123
|
+
message = record.message,
|
124
|
+
# timestamp = record.asctime,
|
125
|
+
)
|
126
|
+
|
127
|
+
# The `logging` stdlib module allows you to add extra values
|
128
|
+
# by providing a `extra` key to the `Logger#debug` call (and
|
129
|
+
# friends), which it just adds to the the keys and values to the
|
130
|
+
# `record` object's `#__dict__` (where they better not conflict
|
131
|
+
# with anything else or you'll be in trouble I guess).
|
132
|
+
#
|
133
|
+
# We look for a `payload` key in there.
|
134
|
+
#
|
135
|
+
# Example logging with a payload:
|
136
|
+
#
|
137
|
+
# logger.debug("My message", extras=dict(payload=dict(x=1)))
|
138
|
+
#
|
139
|
+
# Yeah, it sucks... TODO extend Logger or something to make it a
|
140
|
+
# little easier to use?
|
141
|
+
#
|
142
|
+
if 'payload' in record.__dict__:
|
143
|
+
struct['payload'] = record.__dict__['payload']
|
144
|
+
|
145
|
+
string = json.dumps(struct)
|
146
|
+
self.send(string)
|
147
|
+
except (KeyboardInterrupt, SystemExit):
|
148
|
+
raise
|
149
|
+
except:
|
150
|
+
raise
|
151
|
+
# self.handleError(record)
|
data/lib/qb.rb
CHANGED
@@ -7,6 +7,7 @@
|
|
7
7
|
# Deps
|
8
8
|
# -----------------------------------------------------------------------
|
9
9
|
require 'nrser'
|
10
|
+
require 'nrser/core_ext'
|
10
11
|
|
11
12
|
# Project / Package
|
12
13
|
# -----------------------------------------------------------------------
|
@@ -15,12 +16,13 @@ require 'qb/python'
|
|
15
16
|
require 'qb/version'
|
16
17
|
require 'qb/util'
|
17
18
|
require 'qb/path'
|
19
|
+
require 'qb/data'
|
20
|
+
require 'qb/docker'
|
18
21
|
|
19
22
|
|
20
23
|
# Refinements
|
21
24
|
# =======================================================================
|
22
25
|
|
23
|
-
using NRSER
|
24
26
|
using NRSER::Types
|
25
27
|
|
26
28
|
|
@@ -78,8 +80,6 @@ require 'qb/repo'
|
|
78
80
|
require 'qb/cli'
|
79
81
|
|
80
82
|
require 'qb/ansible'
|
81
|
-
# Depreciated namespace:
|
82
|
-
require 'qb/ansible_module'
|
83
83
|
|
84
84
|
require 'qb/package'
|
85
85
|
|
@@ -9,7 +9,7 @@ require 'cmds'
|
|
9
9
|
|
10
10
|
# package
|
11
11
|
require 'qb/util/bundler'
|
12
|
-
require 'qb/
|
12
|
+
require 'qb/ipc/stdio/server'
|
13
13
|
|
14
14
|
|
15
15
|
module QB; end
|
@@ -225,22 +225,13 @@ class QB::Ansible::Cmds::Playbook < ::Cmds
|
|
225
225
|
before_spawn
|
226
226
|
|
227
227
|
QB::Util::Bundler.with_clean_env do
|
228
|
-
#
|
229
|
-
|
230
|
-
stdio_out_services = {'out' => $stdout, 'err' => $stderr}.
|
231
|
-
map {|name, dest|
|
232
|
-
QB::Util::STDIO::OutService.new(name, dest).tap { |s| s.open! }
|
233
|
-
}
|
234
|
-
|
235
|
-
# and an in service so that modules can prompt for user input
|
236
|
-
user_in_service = QB::Util::STDIO::InService.new('in', $stdin).
|
237
|
-
tap { |s| s.open! }
|
228
|
+
# Start the STDIO server
|
229
|
+
stdio_server = QB::IPC::STDIO::Server.new.start!
|
238
230
|
|
239
231
|
status = super *args, **kwds, &input_block
|
240
232
|
|
241
|
-
#
|
242
|
-
|
243
|
-
user_in_service.close!
|
233
|
+
# ...and stop it
|
234
|
+
stdio_server.stop!
|
244
235
|
|
245
236
|
# and return the status
|
246
237
|
status
|
data/lib/qb/ansible/env.rb
CHANGED
@@ -48,11 +48,23 @@ class QB::Ansible::Env
|
|
48
48
|
attr_reader :filter_plugins
|
49
49
|
|
50
50
|
|
51
|
-
#
|
52
|
-
#
|
51
|
+
# Paths to search for Ansible/Jinja2 "Lookup Plugins"
|
52
|
+
#
|
53
|
+
# @return [Array<Pathname>]
|
54
|
+
#
|
53
55
|
attr_reader :lookup_plugins
|
54
56
|
|
55
57
|
|
58
|
+
# Paths to search for Ansible/Jinja2 "Test Plugins"
|
59
|
+
#
|
60
|
+
# @return [Array<Pathname>]
|
61
|
+
#
|
62
|
+
attr_reader :test_plugins
|
63
|
+
|
64
|
+
|
65
|
+
attr_reader :python_path
|
66
|
+
|
67
|
+
|
56
68
|
# `ANSIBLE_CONFIG_<name>=<value>` ENV var values.
|
57
69
|
#
|
58
70
|
# @see http://docs.ansible.com/ansible/latest/intro_configuration.html
|
@@ -80,11 +92,20 @@ class QB::Ansible::Env
|
|
80
92
|
]
|
81
93
|
|
82
94
|
@filter_plugins = [
|
83
|
-
QB::ROOT.join('plugins', '
|
95
|
+
QB::ROOT.join('plugins', 'filter'),
|
84
96
|
]
|
85
97
|
|
86
98
|
@lookup_plugins = [
|
87
|
-
QB::ROOT.join('plugins', '
|
99
|
+
QB::ROOT.join('plugins', 'lookup'),
|
100
|
+
]
|
101
|
+
|
102
|
+
@test_plugins = [
|
103
|
+
QB::ROOT.join( 'plugins', 'test' ),
|
104
|
+
]
|
105
|
+
|
106
|
+
@python_path = [
|
107
|
+
QB::ROOT.join( 'lib', 'python' ),
|
108
|
+
*(ENV['PYTHONPATH'] || '').split( ':' ),
|
88
109
|
]
|
89
110
|
|
90
111
|
@config = {}
|
@@ -97,7 +118,7 @@ class QB::Ansible::Env
|
|
97
118
|
# @todo Document to_h method.
|
98
119
|
#
|
99
120
|
# @param [type] arg_name
|
100
|
-
# @todo Add name param description.
|
121
|
+
# @todo Add name param description.''
|
101
122
|
#
|
102
123
|
# @return [return_type]
|
103
124
|
# @todo Document return value.
|
@@ -107,7 +128,8 @@ class QB::Ansible::Env
|
|
107
128
|
:roles_path,
|
108
129
|
:library,
|
109
130
|
:filter_plugins,
|
110
|
-
:lookup_plugins
|
131
|
+
:lookup_plugins,
|
132
|
+
:test_plugins,
|
111
133
|
].map { |name|
|
112
134
|
value = self.send name
|
113
135
|
|
@@ -120,6 +142,14 @@ class QB::Ansible::Env
|
|
120
142
|
hash[ self.class.to_var_name( "CONFIG_#{ name }" ) ] = value.to_s
|
121
143
|
}
|
122
144
|
|
145
|
+
hash[ 'QB_AM_AUTORUN_PATH' ] = \
|
146
|
+
(QB::ROOT / 'load' / 'ansible' / 'module' / 'autorun.rb').to_s
|
147
|
+
|
148
|
+
hash[ 'QB_AM_SCRIPT_PATH' ] = \
|
149
|
+
(QB::ROOT / 'load' / 'ansible' / 'module' / 'script.rb').to_s
|
150
|
+
|
151
|
+
hash[ 'PYTHONPATH' ] = python_path.join ':'
|
152
|
+
|
123
153
|
hash
|
124
154
|
end # #to_h
|
125
155
|
|
data/lib/qb/ansible/module.rb
CHANGED
@@ -1,12 +1,25 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# Requirements
|
5
|
+
# =======================================================================
|
6
|
+
|
7
|
+
# Stdlib
|
8
|
+
# -----------------------------------------------------------------------
|
9
|
+
|
1
10
|
require 'json'
|
2
11
|
require 'pp'
|
3
12
|
|
13
|
+
# Deps
|
14
|
+
# ----------------------------------------------------------------------------
|
4
15
|
|
5
|
-
|
6
|
-
|
16
|
+
require 'nrser'
|
17
|
+
require 'nrser/props/immutable/instance_variables'
|
7
18
|
|
8
|
-
|
9
|
-
|
19
|
+
# Project / Package
|
20
|
+
# -----------------------------------------------------------------------
|
21
|
+
|
22
|
+
require 'qb/ipc/stdio/client'
|
10
23
|
|
11
24
|
|
12
25
|
# Declarations
|
@@ -16,147 +29,358 @@ module QB; end
|
|
16
29
|
module QB::Ansible; end
|
17
30
|
|
18
31
|
|
32
|
+
# Refinements
|
33
|
+
# =======================================================================
|
34
|
+
|
35
|
+
using NRSER::Types
|
36
|
+
|
37
|
+
|
19
38
|
# Definitions
|
20
39
|
# =====================================================================
|
21
40
|
|
41
|
+
module QB
|
42
|
+
module Ansible
|
22
43
|
class QB::Ansible::Module
|
23
44
|
|
24
|
-
#
|
25
|
-
#
|
26
|
-
|
27
|
-
|
45
|
+
# Sub-Tree Requirements
|
46
|
+
# ============================================================================
|
47
|
+
|
48
|
+
require_relative './module/response'
|
28
49
|
|
29
50
|
|
30
|
-
#
|
31
|
-
#
|
51
|
+
# Mixins
|
52
|
+
# ============================================================================
|
32
53
|
|
33
|
-
|
34
|
-
hash.map {|k, v| [k.to_s, v]}.to_h
|
35
|
-
end
|
54
|
+
include NRSER::Props::Immutable::InstanceVariables
|
36
55
|
|
56
|
+
include NRSER::Log::Mixin
|
37
57
|
|
38
|
-
def self.arg name, type
|
39
|
-
@@arg_types[name.to_sym] = type
|
40
|
-
end
|
41
58
|
|
42
|
-
|
43
|
-
# Construction
|
59
|
+
# Class Methods
|
44
60
|
# =====================================================================
|
45
61
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
# if QB_STDIO_ env vars are set send stdout and stderr
|
64
|
-
# to those sockets to print in the parent process
|
65
|
-
|
66
|
-
if ENV['QB_STDIO_ERR']
|
67
|
-
@qb_stdio_err = $stderr = UNIXSocket.new ENV['QB_STDIO_ERR']
|
62
|
+
module Formatters
|
63
|
+
class Processor < SemanticLogger::Formatters::Default
|
64
|
+
|
65
|
+
def backtrace_to_s
|
66
|
+
lines = log.backtrace_to_s.lines
|
67
|
+
|
68
|
+
if lines.length > 42
|
69
|
+
lines = [
|
70
|
+
*lines[0..21],
|
71
|
+
"\n# ...\n\n",
|
72
|
+
*lines[-21..-1]
|
73
|
+
]
|
74
|
+
end
|
75
|
+
|
76
|
+
lines.join
|
77
|
+
end
|
68
78
|
|
69
|
-
|
79
|
+
# Exception
|
80
|
+
def exception
|
81
|
+
"-- Exception: #{log.exception.class}: #{log.exception.message}\n#{backtrace_to_s}" if log.exception
|
82
|
+
end
|
70
83
|
end
|
71
84
|
|
72
|
-
|
73
|
-
|
85
|
+
class JSON < SemanticLogger::Formatters::Raw
|
86
|
+
# Default JSON time format is ISO8601
|
87
|
+
def initialize time_format: :iso_8601,
|
88
|
+
log_host: true,
|
89
|
+
log_application: true,
|
90
|
+
time_key: :timestamp
|
91
|
+
super(
|
92
|
+
time_format: time_format,
|
93
|
+
log_host: log_host,
|
94
|
+
log_application: log_application,
|
95
|
+
time_key: time_key,
|
96
|
+
)
|
97
|
+
end
|
74
98
|
|
75
|
-
|
99
|
+
def call log, logger
|
100
|
+
raw = super( log, logger )
|
101
|
+
|
102
|
+
begin
|
103
|
+
raw.to_json
|
104
|
+
rescue Exception => error
|
105
|
+
# SemanticLogger::Processor.instance.appender.logger.warn \
|
106
|
+
# "Unable to JSON encode for logging", raw: raw
|
107
|
+
|
108
|
+
$stderr.puts "Unable to JSON encode log"
|
109
|
+
$stderr.puts raw.pretty_inspect
|
110
|
+
|
111
|
+
raise
|
112
|
+
end
|
113
|
+
end
|
76
114
|
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
def self.setup_io!
|
119
|
+
# Initialize
|
120
|
+
$qb_stdio_client ||= QB::IPC::STDIO::Client.new.connect!
|
77
121
|
|
78
|
-
if
|
79
|
-
|
122
|
+
if $qb_stdio_client.log.connected? && NRSER::Log.appender.nil?
|
123
|
+
# SemanticLogger::Processor.logger = \
|
80
124
|
|
81
|
-
|
125
|
+
SemanticLogger::Processor.instance.appender.logger = \
|
126
|
+
SemanticLogger::Appender::File.new(
|
127
|
+
io: $stderr,
|
128
|
+
level: :warn,
|
129
|
+
formatter: Formatters::Processor.new,
|
130
|
+
)
|
131
|
+
|
132
|
+
NRSER::Log.setup! \
|
133
|
+
application: 'qb',
|
134
|
+
sync: true,
|
135
|
+
dest: {
|
136
|
+
io: $qb_stdio_client.log.socket,
|
137
|
+
formatter: Formatters::JSON.new,
|
138
|
+
}
|
82
139
|
end
|
83
140
|
|
84
|
-
|
85
|
-
|
141
|
+
end # .setup_logging
|
142
|
+
|
143
|
+
|
144
|
+
# Wrap a "run" call with error handling.
|
145
|
+
#
|
146
|
+
# @private
|
147
|
+
#
|
148
|
+
# @param [Proc<() => RESULT] &block
|
149
|
+
#
|
150
|
+
# @return [RESULT]
|
151
|
+
# On success, returns the result of `&block`.
|
152
|
+
#
|
153
|
+
# @raise [SystemExit]
|
154
|
+
# Any exception raised in `&block` is logged at `fatal` level, then
|
155
|
+
# `exit false` is called, raising a {SystemExit} error.
|
156
|
+
#
|
157
|
+
# The only exception: if `&block` raises a {SystemExit} error, that error
|
158
|
+
# is simply re-raised without any logging. This should allow nesting
|
159
|
+
# {.handle_run_error} calls, since the first `rescue` will log any
|
160
|
+
# error and raise {SystemExit}, which will then simply be bubbled-up
|
161
|
+
# by {.handle_run_error} wrappers further up the call chain.
|
162
|
+
#
|
163
|
+
def self.handle_run_error &block
|
164
|
+
begin
|
165
|
+
block.call
|
166
|
+
rescue SystemExit => error
|
167
|
+
# Bubble {SystemExit} up to exit normally
|
168
|
+
raise
|
169
|
+
rescue Exception => error
|
170
|
+
# Everything else is unexpected, and needs to be logged in a way that's
|
171
|
+
# more useful than the JSON-ified crap Ansible would normally print
|
86
172
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
END
|
173
|
+
# If we don't have a logger setup, log to real `STDERR` so we get
|
174
|
+
# *something* back in the Ansible output, even if it's JSON mess
|
175
|
+
if NRSER::Log.appender.nil?
|
176
|
+
NRSER::Log.setup! application: 'qb', dest: STDERR
|
92
177
|
end
|
93
178
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
binding.erb <<-END
|
98
|
-
Value
|
99
|
-
|
100
|
-
<%= value.pretty_inspect %>
|
101
|
-
|
102
|
-
for argument <%= key.inspect %> is not valid for type
|
103
|
-
|
104
|
-
<%= type %>
|
105
|
-
|
106
|
-
Arguments:
|
107
|
-
|
108
|
-
<%= all_args.pretty_inspect %>
|
109
|
-
|
110
|
-
END
|
111
|
-
end
|
179
|
+
# Log it out
|
180
|
+
logger.fatal error
|
112
181
|
|
113
|
-
|
114
|
-
|
115
|
-
|
182
|
+
# And GTFO
|
183
|
+
exit false
|
184
|
+
end
|
185
|
+
end # .handle_run_error
|
186
|
+
|
187
|
+
private_class_method :handle_run_error
|
188
|
+
|
189
|
+
|
190
|
+
# Is the module being run from Ansible via it's "WANT_JSON" mode?
|
191
|
+
#
|
192
|
+
# Tests if `argv` is a single string argument that is a file path.
|
193
|
+
#
|
194
|
+
# @see http://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html#non-native-want-json-modules
|
195
|
+
#
|
196
|
+
# @param [Array<String>] argv
|
197
|
+
# The CLI argument strings.
|
198
|
+
#
|
199
|
+
# @return [Boolean]
|
200
|
+
# `true` if `argv` looks like it came from Ansible's "WANT_JSON" mode.
|
201
|
+
#
|
202
|
+
def self.WANT_JSON_mode? argv = ARGV
|
203
|
+
ARGV.length == 1 && File.file?( ARGV[0] )
|
204
|
+
end # .WANT_JSON_mode?
|
116
205
|
|
117
206
|
|
118
|
-
|
119
|
-
#
|
207
|
+
# Load args from a file in JSON format.
|
208
|
+
#
|
209
|
+
# @param [String | Pathname] file_path
|
210
|
+
# File path to load from.
|
211
|
+
#
|
212
|
+
# @return [Array<(Hash, Hash?)>]
|
213
|
+
# Tuple of:
|
214
|
+
#
|
215
|
+
# 1. `args:`
|
216
|
+
# - `Hash<String, *>`
|
217
|
+
# 2. `args_source:`
|
218
|
+
# - `nil | Hash{ type: :file, path: String, contents: String }`
|
219
|
+
#
|
220
|
+
def self.load_args_from_JSON_file file_path
|
221
|
+
file_contents = File.read file_path
|
222
|
+
|
223
|
+
args = JSON.load( file_contents ).with_indifferent_access
|
224
|
+
|
225
|
+
t.hash_( keys: t.str ).check( args ) do |type:, value:|
|
226
|
+
binding.erb <<~END
|
227
|
+
JSON file contents must load into a `Hash<String, *>`
|
228
|
+
|
229
|
+
Loaded value (of class <%= value.class %>):
|
230
|
+
|
231
|
+
<%= value.pretty_inspect %>
|
232
|
+
|
233
|
+
END
|
234
|
+
end
|
120
235
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
236
|
+
[ args, { type: :file,
|
237
|
+
path: file_path.to_s,
|
238
|
+
contents: file_contents,
|
239
|
+
} ]
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
# Load the raw arguments.
|
244
|
+
#
|
245
|
+
def self.load_args
|
246
|
+
if WANT_JSON_mode?
|
247
|
+
load_args_from_JSON_file ARGV[0]
|
130
248
|
else
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
249
|
+
load_args_from_CLI_options
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
|
254
|
+
# Run the module!
|
255
|
+
#
|
256
|
+
# @return (see #run!)
|
257
|
+
#
|
258
|
+
def self.run!
|
259
|
+
handle_run_error do
|
260
|
+
setup_io!
|
143
261
|
|
144
|
-
|
145
|
-
|
146
|
-
key, value = arg[2..-1].split( '=', 2 )
|
147
|
-
|
148
|
-
@args[key] = begin
|
149
|
-
JSON.load value
|
150
|
-
rescue
|
151
|
-
value
|
152
|
-
end
|
153
|
-
end
|
154
|
-
end
|
262
|
+
args, args_source = load_args
|
263
|
+
run_from_args! args, args_source: args_source
|
155
264
|
end
|
156
|
-
end #
|
265
|
+
end # .run!
|
266
|
+
|
267
|
+
|
268
|
+
# Create and run an instance and populate it's args by loading JSON from a
|
269
|
+
# file path.
|
270
|
+
#
|
271
|
+
# Used to run via Ansible's "WANT_JSON" mode.
|
272
|
+
#
|
273
|
+
# @see http://docs.ansible.com/ansible/latest/dev_guide/developing_program_flow_modules.html#non-native-want-json-modules
|
274
|
+
#
|
275
|
+
# @param [String | Pathname] file_path
|
276
|
+
# Path to the JSON file containing the args.
|
277
|
+
#
|
278
|
+
# @return (see #run!)
|
279
|
+
#
|
280
|
+
def self.run_from_JSON_args_file! file_path
|
281
|
+
file_contents = File.read file_path
|
157
282
|
|
158
|
-
|
159
|
-
|
283
|
+
args = JSON.load file_contents
|
284
|
+
|
285
|
+
t.hash_( keys: t.str ).check( args ) do |type:, value:|
|
286
|
+
binding.erb <<~END
|
287
|
+
JSON file contents must load into a `Hash<String, *>`
|
288
|
+
|
289
|
+
Loaded value (of class <%= value.class %>):
|
290
|
+
|
291
|
+
<%= value.pretty_inspect %>
|
292
|
+
|
293
|
+
END
|
294
|
+
end
|
295
|
+
|
296
|
+
run_from_args! args,
|
297
|
+
args_source: {
|
298
|
+
type: :file,
|
299
|
+
path: file_path,
|
300
|
+
contents: file_contents,
|
301
|
+
}
|
302
|
+
end # .run_from_JSON_args_file!
|
303
|
+
|
304
|
+
|
305
|
+
# Run from a hash-like of argument names mapped to values, with optional
|
306
|
+
# info about the source of the arguments.
|
307
|
+
#
|
308
|
+
# @param [#each_pair] args
|
309
|
+
# Argument names (String or Symbol) mapped to their value data.
|
310
|
+
#
|
311
|
+
# @return (see #run!)
|
312
|
+
#
|
313
|
+
def self.run_from_args! args, args_source: nil
|
314
|
+
logger.trace "Running from args",
|
315
|
+
args: args,
|
316
|
+
args_source: args_source
|
317
|
+
|
318
|
+
instance = self.from_data args
|
319
|
+
instance.args_source = args_source
|
320
|
+
instance.args = args
|
321
|
+
instance.run!
|
322
|
+
end # .run_from_args!
|
323
|
+
|
324
|
+
|
325
|
+
# @todo Document arg method.
|
326
|
+
#
|
327
|
+
# @param [type] arg_name
|
328
|
+
# @todo Add name param description.
|
329
|
+
#
|
330
|
+
# @return [return_type]
|
331
|
+
# @todo Document return value.
|
332
|
+
#
|
333
|
+
def self.arg *args, **opts
|
334
|
+
name, opts = t.match args.length,
|
335
|
+
# Normal {.prop} form
|
336
|
+
1, ->( _ ){ [ args[0], opts ] },
|
337
|
+
|
338
|
+
# Backwards-compatible form
|
339
|
+
2, ->( _ ){ [ args[0], opts.merge( type: args[1] ) ] }
|
340
|
+
|
341
|
+
prop name, **opts
|
342
|
+
end # .arg
|
343
|
+
|
344
|
+
|
345
|
+
# Attributes
|
346
|
+
# ==========================================================================
|
347
|
+
|
348
|
+
# Optional information on the source of the arguments.
|
349
|
+
#
|
350
|
+
# @return [nil | Hash<Symbol, Object>]
|
351
|
+
#
|
352
|
+
attr_accessor :args_source
|
353
|
+
|
354
|
+
|
355
|
+
# The raw parsed arguments. Used for backwards-compatibility with how
|
356
|
+
# {QB::Ansible::Module} used to work before {NRSER::Props} and {#arg}.
|
357
|
+
#
|
358
|
+
# @todo
|
359
|
+
# May want to get rid of this once using props is totally flushed out.
|
360
|
+
#
|
361
|
+
# It should at least be deal with in the constructor somehow so this
|
362
|
+
# can be changed to an `attr_reader`.
|
363
|
+
#
|
364
|
+
# @return [Hash<String, VALUE>]
|
365
|
+
#
|
366
|
+
attr_accessor :args
|
367
|
+
|
368
|
+
|
369
|
+
# The response that will be returned to Ansible (JSON-encoded and written
|
370
|
+
# to `STDOUT`).
|
371
|
+
#
|
372
|
+
# @return [QB::Ansible::Module::Response]
|
373
|
+
#
|
374
|
+
attr_reader :response
|
375
|
+
|
376
|
+
|
377
|
+
# Construction
|
378
|
+
# =====================================================================
|
379
|
+
|
380
|
+
def initialize values = {}
|
381
|
+
initialize_props values
|
382
|
+
@response = QB::Ansible::Module::Response.new
|
383
|
+
end
|
160
384
|
|
161
385
|
|
162
386
|
# Instance Methods
|
@@ -176,7 +400,7 @@ class QB::Ansible::Module
|
|
176
400
|
# listen to, and we provide those file paths via environment variables
|
177
401
|
# so modules can pick those up and interact with those streams, allowing
|
178
402
|
# them to act like regular scripts inside Ansible-world (see
|
179
|
-
# QB::
|
403
|
+
# QB::IPC::STDIO for details and implementation).
|
180
404
|
#
|
181
405
|
# We use those channels if present to provide logging mechanisms.
|
182
406
|
#
|
@@ -187,37 +411,54 @@ class QB::Ansible::Module
|
|
187
411
|
# @param args see QB.debug
|
188
412
|
#
|
189
413
|
def debug *args
|
190
|
-
|
191
|
-
header = "<QB::Ansible::Module #{ self.class.name }>"
|
192
|
-
|
193
|
-
if args[0].is_a? String
|
194
|
-
header += " " + args.shift
|
195
|
-
end
|
196
|
-
|
197
|
-
QB.debug header, *args
|
198
|
-
end
|
414
|
+
logger.debug payload: args
|
199
415
|
end
|
200
416
|
|
417
|
+
|
418
|
+
# Old logging function - use `#logger.info` instead.
|
419
|
+
#
|
420
|
+
# @deprecated
|
421
|
+
#
|
201
422
|
def info msg
|
202
|
-
|
203
|
-
$stderr.puts msg
|
204
|
-
end
|
423
|
+
logger.info msg
|
205
424
|
end
|
206
425
|
|
207
|
-
|
426
|
+
|
427
|
+
# Append a warning message to the {#response}'s {Response#warnings}
|
428
|
+
# array and log it.
|
429
|
+
#
|
430
|
+
# @todo
|
431
|
+
# Should be incorporated into {#logger}? Seems like it would need one of:
|
432
|
+
#
|
433
|
+
# 1. `on_...` hooks, like `Logger#on_warn`, etc.
|
434
|
+
#
|
435
|
+
# This might be nice but I'd rather hold off on throwing more shit
|
436
|
+
# into {NRSER::Log::Logger} for the time being if possible.
|
437
|
+
#
|
438
|
+
# 2. Adding a custom appender when we run a module that has a ref to
|
439
|
+
# the module instance and so it's {Response}.
|
440
|
+
#
|
441
|
+
#
|
442
|
+
# @param [String] msg
|
443
|
+
# Non-empty string.
|
444
|
+
#
|
445
|
+
# @return [nil]
|
446
|
+
#
|
208
447
|
def warn msg
|
209
|
-
|
448
|
+
logger.warn msg
|
449
|
+
response.warnings << msg
|
450
|
+
nil
|
210
451
|
end
|
211
452
|
|
212
453
|
|
213
|
-
def run
|
454
|
+
def run!
|
214
455
|
result = main
|
215
456
|
|
216
457
|
case result
|
217
458
|
when nil
|
218
459
|
# pass
|
219
460
|
when Hash
|
220
|
-
|
461
|
+
response.facts.merge! result
|
221
462
|
else
|
222
463
|
raise "result of #main should be nil or Hash, found #{ result.inspect }"
|
223
464
|
end
|
@@ -225,40 +466,43 @@ class QB::Ansible::Module
|
|
225
466
|
done
|
226
467
|
end
|
227
468
|
|
469
|
+
|
228
470
|
def changed! facts = {}
|
229
|
-
|
230
|
-
|
471
|
+
response.changed = true
|
472
|
+
|
473
|
+
unless facts.empty?
|
474
|
+
response.facts.merge! facts
|
475
|
+
end
|
476
|
+
|
231
477
|
done
|
232
478
|
end
|
233
479
|
|
480
|
+
|
234
481
|
def done
|
235
|
-
exit_json
|
236
|
-
ansible_facts: self.class.stringify_keys(@facts),
|
237
|
-
warnings: @warnings
|
482
|
+
exit_json response.to_data( add_class: false ).compact
|
238
483
|
end
|
239
484
|
|
485
|
+
|
240
486
|
def exit_json hash
|
241
487
|
# print JSON response to process' actual STDOUT (instead of $stdout,
|
242
488
|
# which may be pointing to the qb parent process)
|
243
|
-
STDOUT.print JSON.pretty_generate(
|
244
|
-
|
245
|
-
[
|
246
|
-
[:stdin, @qb_stdio_in],
|
247
|
-
[:stdout, @qb_stdio_out],
|
248
|
-
[:stderr, @qb_stdio_err],
|
249
|
-
].each do |name, socket|
|
250
|
-
if socket
|
251
|
-
debug "Flushing socket #{ name }."
|
252
|
-
socket.flush
|
253
|
-
debug "Closing #{ name } socket at #{ socket.path.to_s }."
|
254
|
-
socket.close
|
255
|
-
end
|
256
|
-
end
|
489
|
+
STDOUT.print JSON.pretty_generate( hash.stringify_keys )
|
257
490
|
|
258
|
-
exit
|
491
|
+
exit true
|
259
492
|
end
|
260
493
|
|
261
|
-
|
262
|
-
|
494
|
+
|
495
|
+
def fail msg, **values
|
496
|
+
fail_response = QB::Ansible::Module::Response.new \
|
497
|
+
failed: true,
|
498
|
+
msg: msg.to_s,
|
499
|
+
warnings: response.warnings,
|
500
|
+
depreciations: response.depreciations
|
501
|
+
|
502
|
+
STDOUT.print \
|
503
|
+
JSON.pretty_generate( fail_response.to_data( add_class: false ).compact )
|
504
|
+
|
505
|
+
exit false
|
263
506
|
end
|
264
|
-
|
507
|
+
|
508
|
+
end; end; end # class QB::Ansible::Module
|