qb 0.3.25 → 0.4.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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/ansible.cfg +10 -1
  4. data/exe/.qb_interop_receive +3 -10
  5. data/exe/qb +8 -2
  6. data/lib/python/qb/__init__.py +6 -0
  7. data/{roles/qb/ruby/rspec/setup/tasks/persistence.yml → lib/python/qb/ansible/__init__.py} +0 -0
  8. data/lib/python/qb/ansible/modules/__init__.py +0 -0
  9. data/lib/python/qb/ansible/modules/docker/__init__.py +0 -0
  10. data/lib/python/qb/ansible/modules/docker/client.py +177 -0
  11. data/lib/python/qb/ansible/modules/docker/image_manager.py +754 -0
  12. data/lib/python/qb/ipc/__init__.py +0 -0
  13. data/lib/python/qb/ipc/stdio/__init__.py +99 -0
  14. data/lib/python/qb/ipc/stdio/logging.py +151 -0
  15. data/lib/qb.rb +3 -3
  16. data/lib/qb/ansible/cmds/playbook.rb +5 -14
  17. data/lib/qb/ansible/env.rb +36 -6
  18. data/lib/qb/ansible/module.rb +396 -152
  19. data/lib/qb/ansible/module/response.rb +195 -0
  20. data/lib/qb/ansible/modules.rb +42 -0
  21. data/lib/qb/ansible/modules/docker/image.rb +273 -0
  22. data/lib/qb/cli.rb +5 -18
  23. data/lib/qb/cli/run.rb +2 -2
  24. data/lib/qb/data.rb +22 -0
  25. data/lib/qb/data/immutable.rb +39 -0
  26. data/lib/qb/docker.rb +2 -0
  27. data/lib/qb/docker/cli.rb +430 -0
  28. data/lib/qb/docker/image.rb +207 -0
  29. data/lib/qb/docker/image/name.rb +309 -0
  30. data/lib/qb/docker/image/tag.rb +113 -0
  31. data/lib/qb/docker/repo.rb +0 -0
  32. data/lib/qb/errors.rb +17 -3
  33. data/lib/qb/execution.rb +83 -0
  34. data/lib/qb/ipc.rb +48 -0
  35. data/lib/qb/ipc/stdio.rb +32 -0
  36. data/lib/qb/ipc/stdio/client.rb +267 -0
  37. data/lib/qb/ipc/stdio/server.rb +229 -0
  38. data/lib/qb/ipc/stdio/server/in_service.rb +18 -0
  39. data/lib/qb/ipc/stdio/server/log_service.rb +168 -0
  40. data/lib/qb/ipc/stdio/server/out_service.rb +20 -0
  41. data/lib/qb/ipc/stdio/server/service.rb +229 -0
  42. data/lib/qb/options.rb +360 -502
  43. data/lib/qb/options/option.rb +293 -115
  44. data/lib/qb/options/option/option_parser_concern.rb +228 -0
  45. data/lib/qb/options/types.rb +73 -0
  46. data/lib/qb/package.rb +0 -1
  47. data/lib/qb/package/version.rb +179 -58
  48. data/lib/qb/package/version/from.rb +192 -51
  49. data/lib/qb/package/version/leveled.rb +1 -1
  50. data/lib/qb/path.rb +3 -2
  51. data/lib/qb/repo/git.rb +9 -85
  52. data/lib/qb/role/default_dir.rb +2 -2
  53. data/lib/qb/role/errors.rb +2 -8
  54. data/lib/qb/util.rb +1 -2
  55. data/lib/qb/util/bundler.rb +73 -43
  56. data/lib/qb/util/decorators.rb +99 -0
  57. data/lib/qb/util/interop.rb +7 -8
  58. data/lib/qb/util/resource.rb +12 -13
  59. data/lib/qb/version.rb +10 -0
  60. data/library/path_facts +5 -10
  61. data/library/qb.module.rb +105 -0
  62. data/library/stream +6 -26
  63. data/load/ansible/module/autorun.rb +25 -0
  64. data/load/ansible/module/script.rb +123 -0
  65. data/load/rebundle.rb +39 -0
  66. data/plugins/filter/dict_filters.py +56 -0
  67. data/plugins/{filter_plugins/path_plugins.py → filter/path_filters.py} +0 -0
  68. data/plugins/{filter_plugins/ruby_interop_plugins.py → filter/ruby_interop_filters.py} +1 -17
  69. data/plugins/{filter_plugins/string_plugins.py → filter/string_filters.py} +1 -20
  70. data/plugins/{filter_plugins/version_plugins.py → filter/version_filters.py} +3 -18
  71. data/plugins/{lookup_plugins/every.py → lookup/every_lookups.py} +0 -0
  72. data/plugins/{lookup_plugins/resolve.py → lookup/resolve_lookups.py} +0 -0
  73. data/plugins/{lookup_plugins/version.py → lookup/version_lookups.py} +0 -16
  74. data/plugins/test/dict_tests.py +36 -0
  75. data/plugins/test/string_tests.py +36 -0
  76. data/qb.gemspec +7 -3
  77. data/roles/nrser.rb/library/set_fact_with_ruby.rb +3 -9
  78. data/roles/nrser.state_mate/library/state +3 -17
  79. data/roles/qb/call/meta/qb.yml +1 -1
  80. data/roles/qb/dev/ref/repo/git/meta/qb.yml +1 -1
  81. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/defaults/main.yml +1 -1
  82. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/main.yml +3 -2
  83. data/roles/qb/{ruby/rspec/setup → docker/mac/kubernetes}/meta/qb.yml +12 -7
  84. data/roles/qb/docker/mac/kubernetes/tasks/main.yml +45 -0
  85. data/roles/qb/git/check/clean/meta/qb.yml +1 -1
  86. data/roles/qb/git/ignore/meta/qb +10 -3
  87. data/roles/qb/git/submodule/update/library/git_submodule_update +17 -27
  88. data/roles/qb/github/pages/setup/meta/qb.yml +1 -1
  89. data/roles/qb/labs/atom/apm/meta/qb.yml +1 -1
  90. data/roles/qb/osx/git/change_case/meta/qb.yml +1 -1
  91. data/roles/qb/osx/notif/meta/qb.yml +1 -1
  92. data/roles/qb/pkg/bump/library/bump +4 -16
  93. data/roles/qb/role/qb/defaults/main.yml +2 -0
  94. data/roles/qb/role/qb/meta/qb.yml +10 -5
  95. data/roles/qb/role/qb/templates/qb.yml.j2 +7 -2
  96. data/roles/qb/role/templates/library/module.rb.j2 +12 -23
  97. data/roles/qb/role/templates/meta/main.yml.j2 +14 -1
  98. data/roles/qb/ruby/bundler/meta/qb.yml +1 -1
  99. data/roles/qb/ruby/dependency/meta/qb.yml +1 -1
  100. data/roles/qb/ruby/gem/bin_stubs/meta/qb.yml +1 -1
  101. data/roles/qb/ruby/gem/bin_stubs/templates/console +8 -2
  102. data/roles/qb/ruby/gem/build/meta/qb.yml +1 -1
  103. data/roles/qb/ruby/gem/new/meta/qb.yml +1 -1
  104. data/roles/qb/ruby/nrser/rspex/generate/meta/qb.yml +5 -5
  105. data/roles/qb/ruby/nrser/rspex/issue/meta/qb.yml +1 -1
  106. data/roles/qb/ruby/yard/clean/meta/qb.yml +1 -1
  107. data/roles/qb/ruby/yard/config/library/yard.get_output_dir +5 -15
  108. data/roles/qb/ruby/yard/config/meta/qb.yml +1 -1
  109. data/roles/qb/ruby/yard/setup/meta/qb.yml +1 -1
  110. metadata +71 -22
  111. data/lib/qb/ansible_module.rb +0 -5
  112. data/lib/qb/util/stdio.rb +0 -187
  113. 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/util/stdio'
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
- # boot up stdio out services so that ansible modules can stream to our
229
- # stdout and stderr to print stuff (including debug lines) in real-time
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
- # close the stdio services
242
- stdio_out_services.each {|s| s.close! }
243
- user_in_service.close!
233
+ # ...and stop it
234
+ stdio_server.stop!
244
235
 
245
236
  # and return the status
246
237
  status
@@ -48,11 +48,23 @@ class QB::Ansible::Env
48
48
  attr_reader :filter_plugins
49
49
 
50
50
 
51
- # @!attribute [r] lookup_plugins
52
- # @return [Array<Pathname>]
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', 'filter_plugins'),
95
+ QB::ROOT.join('plugins', 'filter'),
84
96
  ]
85
97
 
86
98
  @lookup_plugins = [
87
- QB::ROOT.join('plugins', 'lookup_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
 
@@ -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
- # Refinements
6
- # =======================================================================
16
+ require 'nrser'
17
+ require 'nrser/props/immutable/instance_variables'
7
18
 
8
- using NRSER
9
- using NRSER::Types
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
- # Class Variables
25
- # =====================================================================
26
-
27
- @@arg_types = {}
45
+ # Sub-Tree Requirements
46
+ # ============================================================================
47
+
48
+ require_relative './module/response'
28
49
 
29
50
 
30
- # Class Methods
31
- # =====================================================================
51
+ # Mixins
52
+ # ============================================================================
32
53
 
33
- def self.stringify_keys hash
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
- def initialize
47
- @changed = false
48
- # @input_file = ARGV[0]
49
- # @input = File.read @input_file
50
- # @args = JSON.load @input
51
- init_set_args!
52
-
53
- @facts = {}
54
- @warnings = []
55
-
56
- @qb_stdio_out = nil
57
- @qb_stdio_err = nil
58
- @qb_stdio_in = nil
59
-
60
- # debug "HERE!"
61
- # debug ENV
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
- debug "Connected to QB stderr stream at #{ ENV['QB_STDIO_ERR'] } #{ @qb_stdio_err.path }."
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
- if ENV['QB_STDIO_OUT']
73
- @qb_stdio_out = $stdout = UNIXSocket.new ENV['QB_STDIO_OUT']
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
- debug "Connected to QB stdout stream at #{ ENV['QB_STDIO_OUT'] }."
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 ENV['QB_STDIO_IN']
79
- @qb_stdio_in = UNIXSocket.new ENV['QB_STDIO_IN']
122
+ if $qb_stdio_client.log.connected? && NRSER::Log.appender.nil?
123
+ # SemanticLogger::Processor.logger = \
80
124
 
81
- debug "Connected to QB stdin stream at #{ ENV['QB_STDIO_IN'] }."
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
- @@arg_types.each {|key, type|
85
- var_name = "@#{ key.to_s }"
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
- unless instance_variable_get(var_name).nil?
88
- raise ArgumentError.new NRSER.squish <<-END
89
- an instance variable named #{ var_name } exists
90
- with value #{ instance_variable_get(var_name).inspect }
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
- value = type.check( @args[key.to_s] ) do |type:, value:|
95
- all_args = @args
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
- instance_variable_set var_name, value
114
- }
115
- end
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
- protected
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
- def init_set_args!
122
- if ARGV.length == 1 && File.file?( ARGV[0] )
123
- # "Standard" Ansible-invoked mode, where the args written in JSON format
124
- # to a file and the path is provided as the only CLI argument
125
- #
126
- @input_file = ARGV[0]
127
- @input = File.read @input_file
128
- @args = JSON.load @input
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
- # QB-specific "fiddle-mode": if we don't have a single valid file path
132
- # as CLI arguments, parse the CLI options we have in the common
133
- #
134
- # `--name=value`
135
- #
136
- # format into the `@args` hash.
137
- #
138
- # This lets us run the module file **directly** from the terminal, which
139
- # is just a quick and dirty way of flushing things out.
140
- #
141
- @fiddle_mode = true
142
- @args = {}
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
- ARGV.each do |arg|
145
- if arg.start_with? '--'
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 # #init_set_args!
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
- # end protected
159
- public
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::Util::STDIO for details and implementation).
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
- if @qb_stdio_err
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
- if @qb_stdio_err
203
- $stderr.puts msg
204
- end
423
+ logger.info msg
205
424
  end
206
425
 
207
- # Append a warning message to @warnings.
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
- @warnings << msg
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
- @facts.merge! result
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
- @changed = true
230
- @facts.merge! facts
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 changed: @changed,
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(self.class.stringify_keys(hash))
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 0
491
+ exit true
259
492
  end
260
493
 
261
- def fail msg
262
- exit_json failed: true, msg: msg, warnings: @warnings
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
- end # class QB::Ansible::Module
507
+
508
+ end; end; end # class QB::Ansible::Module