qb 0.3.25 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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