glazed 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +18 -0
- data/LICENSE.txt +202 -0
- data/NOTICE +16 -0
- data/README.md +3 -0
- data/Rakefile +12 -0
- data/exe/glazed +8 -0
- data/lib/glazed/cli_session.rb +178 -0
- data/lib/glazed/collector.rb +113 -0
- data/lib/glazed/command/base.rb +146 -0
- data/lib/glazed/command/orchestrate.rb +78 -0
- data/lib/glazed/command/run.rb +51 -0
- data/lib/glazed/command/send_signal.rb +69 -0
- data/lib/glazed/command/status.rb +179 -0
- data/lib/glazed/command/version.rb +23 -0
- data/lib/glazed/command.rb +39 -0
- data/lib/glazed/command_result.rb +26 -0
- data/lib/glazed/dialect/actions.rb +95 -0
- data/lib/glazed/dialect/communication_config.rb +63 -0
- data/lib/glazed/dialect/interaction.rb +32 -0
- data/lib/glazed/dialect/run.rb +84 -0
- data/lib/glazed/dialect/runbook.rb +97 -0
- data/lib/glazed/dialect/sensitive_pattern.rb +31 -0
- data/lib/glazed/dialect.rb +12 -0
- data/lib/glazed/dialects/awplus.rb +84 -0
- data/lib/glazed/dialects/base.rb +246 -0
- data/lib/glazed/dialects/ios.rb +173 -0
- data/lib/glazed/dialects/junos.rb +78 -0
- data/lib/glazed/dialects/nec_ix.rb +71 -0
- data/lib/glazed/dialects/wlc.rb +56 -0
- data/lib/glazed/dialects.rb +65 -0
- data/lib/glazed/finder.rb +62 -0
- data/lib/glazed/must.rb +54 -0
- data/lib/glazed/orchestrator/policy.rb +184 -0
- data/lib/glazed/orchestrator.rb +288 -0
- data/lib/glazed/plugin_registry.rb +70 -0
- data/lib/glazed/proto.rb +20 -0
- data/lib/glazed/proto_json.rb +34 -0
- data/lib/glazed/redactor.rb +158 -0
- data/lib/glazed/runner.rb +176 -0
- data/lib/glazed/secret.rb +74 -0
- data/lib/glazed/secrets.rb +59 -0
- data/lib/glazed/shell_command.rb +90 -0
- data/lib/glazed/sources/base.rb +79 -0
- data/lib/glazed/sources/command.rb +67 -0
- data/lib/glazed/sources/inline.rb +56 -0
- data/lib/glazed/sources/json_files.rb +87 -0
- data/lib/glazed/sources.rb +105 -0
- data/lib/glazed/state_backends/base.rb +72 -0
- data/lib/glazed/state_backends/sqlite.rb +299 -0
- data/lib/glazed/state_backends.rb +75 -0
- data/lib/glazed/step_context.rb +65 -0
- data/lib/glazed/steps/base.rb +89 -0
- data/lib/glazed/steps/batch_shell.rb +57 -0
- data/lib/glazed/steps/fetch.rb +67 -0
- data/lib/glazed/steps/publish.rb +94 -0
- data/lib/glazed/steps/redact.rb +77 -0
- data/lib/glazed/steps/shell.rb +107 -0
- data/lib/glazed/steps.rb +83 -0
- data/lib/glazed/storages/base.rb +167 -0
- data/lib/glazed/storages/filesystem.rb +144 -0
- data/lib/glazed/storages/git.rb +181 -0
- data/lib/glazed/storages/github.rb +269 -0
- data/lib/glazed/storages/memory.rb +125 -0
- data/lib/glazed/storages.rb +88 -0
- data/lib/glazed/transports/base.rb +103 -0
- data/lib/glazed/transports/ssh.rb +177 -0
- data/lib/glazed/transports/telnet.rb +100 -0
- data/lib/glazed/transports.rb +28 -0
- data/lib/glazed/v1/config_pb.rb +25 -0
- data/lib/glazed/v1/inventory_pb.rb +27 -0
- data/lib/glazed/v1/orchestrator_pb.rb +27 -0
- data/lib/glazed/v1/redaction_pb.rb +23 -0
- data/lib/glazed/v1/secret_pb.rb +22 -0
- data/lib/glazed/v1/state_pb.rb +25 -0
- data/lib/glazed/v1/steps_pb.rb +25 -0
- data/lib/glazed/v1/storage_pb.rb +34 -0
- data/lib/glazed/v1/workflow_pb.rb +29 -0
- data/lib/glazed/validation.rb +31 -0
- data/lib/glazed/version.rb +9 -0
- data/lib/glazed/workflow.rb +70 -0
- data/lib/glazed.rb +18 -0
- metadata +245 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'google/protobuf'
|
|
8
|
+
require 'glazed'
|
|
9
|
+
require 'glazed/proto'
|
|
10
|
+
require 'glazed/proto_json'
|
|
11
|
+
require 'glazed/validation'
|
|
12
|
+
|
|
13
|
+
module Glazed
|
|
14
|
+
module Sources
|
|
15
|
+
# Inventory provider. Subclasses implement {.from_proto} and {#nodes}.
|
|
16
|
+
class Base
|
|
17
|
+
# Raised when inventory is malformed or a node lacks a name or dialect.
|
|
18
|
+
class InvalidInventory < Glazed::Error; end
|
|
19
|
+
|
|
20
|
+
# Node fields merged key by key with defaults.
|
|
21
|
+
MAP_FIELDS = %w[params secrets].freeze #: Array[String]
|
|
22
|
+
|
|
23
|
+
# Builds a provider from its settings message.
|
|
24
|
+
#
|
|
25
|
+
# @param settings [Google::Protobuf::MessageExts] provider settings from {Proto::V1::InventorySource}
|
|
26
|
+
# @return [Base] the provider
|
|
27
|
+
#: (Google::Protobuf::MessageExts settings) -> Base
|
|
28
|
+
def self.from_proto(settings) = raise NotImplementedError
|
|
29
|
+
|
|
30
|
+
# Reads the nodes.
|
|
31
|
+
#
|
|
32
|
+
# @return [Array<Proto::V1::Node>] nodes, each with a name and a dialect
|
|
33
|
+
#: () -> Array[Proto::V1::Node]
|
|
34
|
+
def nodes = raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
# @return [String] where the nodes come from, for error messages
|
|
37
|
+
#: () -> String
|
|
38
|
+
def description = self.class.name.to_s
|
|
39
|
+
|
|
40
|
+
# Builds the nodes of ProtoJSON of an {Proto::V1::InventoryFile}.
|
|
41
|
+
#
|
|
42
|
+
# @param json [String] the inventory file
|
|
43
|
+
# @param origin [String] where the JSON comes from, for error messages
|
|
44
|
+
# @return [Array<Proto::V1::Node>] nodes with defaults applied
|
|
45
|
+
# @raise [InvalidInventory] when the JSON is malformed
|
|
46
|
+
# @raise [InvalidMessage] when the inventory breaks the rules of its schema
|
|
47
|
+
#: (String json, origin: String) -> Array[Proto::V1::Node]
|
|
48
|
+
private def nodes_from_json(json, origin:)
|
|
49
|
+
Validation.validate!(ProtoJson.load(Proto::V1::InventoryFile, json), origin:)
|
|
50
|
+
document = JSON.parse(json) #: Hash[String, untyped]
|
|
51
|
+
nodes_from(document, origin:)
|
|
52
|
+
rescue Google::Protobuf::ParseError, JSON::ParserError => e
|
|
53
|
+
raise InvalidInventory, "#{origin}: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Builds the nodes of an {Proto::V1::InventoryFile} document, filling
|
|
57
|
+
# fields each node leaves unset from its defaults. The params and secrets
|
|
58
|
+
# maps are merged key by key.
|
|
59
|
+
#
|
|
60
|
+
# @param document [Hash{String => Object}] the inventory file as decoded JSON
|
|
61
|
+
# @param origin [String] where the document comes from, for error messages
|
|
62
|
+
# @return [Array<Proto::V1::Node>] the nodes
|
|
63
|
+
# @raise [InvalidInventory] when a node is malformed
|
|
64
|
+
#: (Hash[String, untyped] document, origin: String) -> Array[Proto::V1::Node]
|
|
65
|
+
private def nodes_from(document, origin:)
|
|
66
|
+
defaults = document.fetch('defaults', {}) #: Hash[String, untyped]
|
|
67
|
+
nodes = document.fetch('nodes', []) #: Array[Hash[String, untyped]]
|
|
68
|
+
nodes.map do |attributes|
|
|
69
|
+
merged = defaults.merge(attributes) do |key, default, value|
|
|
70
|
+
MAP_FIELDS.include?(key) ? default.merge(value) : value
|
|
71
|
+
end
|
|
72
|
+
ProtoJson.load(Proto::V1::Node, JSON.generate(merged))
|
|
73
|
+
rescue Google::Protobuf::ParseError => e
|
|
74
|
+
raise InvalidInventory, "#{origin}: #{e.message}"
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'glazed'
|
|
7
|
+
require 'glazed/must'
|
|
8
|
+
require 'glazed/proto'
|
|
9
|
+
require 'glazed/shell_command'
|
|
10
|
+
require 'glazed/sources'
|
|
11
|
+
require 'glazed/sources/base'
|
|
12
|
+
|
|
13
|
+
module Glazed
|
|
14
|
+
module Sources
|
|
15
|
+
# Inventory printed by a command as ProtoJSON of {Proto::V1::InventoryFile},
|
|
16
|
+
# for nodes kept in another system.
|
|
17
|
+
#
|
|
18
|
+
# @example
|
|
19
|
+
# Glazed::Sources::Command.new(run: 'bin/inventory --site hnd').nodes
|
|
20
|
+
class Command < Base
|
|
21
|
+
# Seconds before the command is killed when the settings set none.
|
|
22
|
+
DEFAULT_TIMEOUT = 300 #: Integer
|
|
23
|
+
|
|
24
|
+
# @param run [String] command line, run by /bin/sh -c
|
|
25
|
+
# @param timeout [Numeric] seconds before the command is killed
|
|
26
|
+
#: (run: String, ?timeout: Numeric) -> void
|
|
27
|
+
def initialize(run:, timeout: DEFAULT_TIMEOUT)
|
|
28
|
+
super()
|
|
29
|
+
@run = run #: String
|
|
30
|
+
@timeout = timeout #: Numeric
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Builds a provider from {Proto::V1::CommandInventory}.
|
|
34
|
+
#
|
|
35
|
+
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::CommandInventory}
|
|
36
|
+
# @return [Command] the provider
|
|
37
|
+
# @raise [Must::Violation] when settings is another message
|
|
38
|
+
#: (Google::Protobuf::MessageExts settings) -> Command
|
|
39
|
+
def self.from_proto(settings)
|
|
40
|
+
command = Must.be_a(settings, Proto::V1::CommandInventory)
|
|
41
|
+
new(run: command.run, timeout: command.timeout_seconds.zero? ? DEFAULT_TIMEOUT : command.timeout_seconds)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# @return [String] command line
|
|
45
|
+
#: String
|
|
46
|
+
attr_reader :run
|
|
47
|
+
|
|
48
|
+
# @return [Numeric] seconds before the command is killed
|
|
49
|
+
#: Numeric
|
|
50
|
+
attr_reader :timeout
|
|
51
|
+
|
|
52
|
+
# Runs the command and reads the nodes it prints.
|
|
53
|
+
#
|
|
54
|
+
# @return [Array<Proto::V1::Node>] nodes with defaults applied
|
|
55
|
+
# @raise [ShellCommand::Failed] when the command exits non-zero
|
|
56
|
+
# @raise [TimeoutError] when the command does not finish in time
|
|
57
|
+
# @raise [InvalidInventory] when the output is not an inventory file
|
|
58
|
+
# @raise [InvalidMessage] when the output breaks the rules of its schema
|
|
59
|
+
#: () -> Array[Proto::V1::Node]
|
|
60
|
+
def nodes = nodes_from_json(ShellCommand.new(run:, env: {}, stdin: '', timeout:).call, origin: description)
|
|
61
|
+
|
|
62
|
+
# @return [String] the command line
|
|
63
|
+
#: () -> String
|
|
64
|
+
def description = "command #{run}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'json'
|
|
7
|
+
require 'glazed/must'
|
|
8
|
+
require 'glazed/proto'
|
|
9
|
+
require 'glazed/proto_json'
|
|
10
|
+
require 'glazed/sources'
|
|
11
|
+
require 'glazed/sources/base'
|
|
12
|
+
require 'glazed/validation'
|
|
13
|
+
|
|
14
|
+
module Glazed
|
|
15
|
+
module Sources
|
|
16
|
+
# Inventory written in the config itself, for setups that keep everything
|
|
17
|
+
# in one file. Defaults apply as in {JsonFiles}.
|
|
18
|
+
#
|
|
19
|
+
# @example Inventory source
|
|
20
|
+
# {"inline": {"defaults": {"dialect": "ios"}, "nodes": [{"name": "rt1"}]}}
|
|
21
|
+
class Inline < Base
|
|
22
|
+
# @param inventory [Proto::V1::InventoryFile] nodes and their defaults
|
|
23
|
+
#: (inventory: Proto::V1::InventoryFile) -> void
|
|
24
|
+
def initialize(inventory:)
|
|
25
|
+
super()
|
|
26
|
+
@inventory = inventory #: Proto::V1::InventoryFile
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Builds a provider from an inline {Proto::V1::InventoryFile}.
|
|
30
|
+
#
|
|
31
|
+
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::InventoryFile}
|
|
32
|
+
# @return [Inline] the provider
|
|
33
|
+
# @raise [Must::Violation] when settings is another message
|
|
34
|
+
# @raise [InvalidMessage] when the inventory breaks the rules of its schema
|
|
35
|
+
#: (Google::Protobuf::MessageExts settings) -> Inline
|
|
36
|
+
def self.from_proto(settings)
|
|
37
|
+
inventory = Must.be_a(settings, Proto::V1::InventoryFile)
|
|
38
|
+
Validation.validate!(inventory, origin: 'inline inventory')
|
|
39
|
+
new(inventory:)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Array<Proto::V1::Node>] the nodes with defaults applied
|
|
43
|
+
# @raise [InvalidInventory] when a node lacks a name or dialect
|
|
44
|
+
#: () -> Array[Proto::V1::Node]
|
|
45
|
+
def nodes
|
|
46
|
+
# ProtoJSON omits unset fields, so defaults merge as they do for files.
|
|
47
|
+
document = JSON.parse(ProtoJson.dump(@inventory)) #: Hash[String, untyped]
|
|
48
|
+
nodes_from(document, origin: description)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [String] where the nodes come from
|
|
52
|
+
#: () -> String
|
|
53
|
+
def description = 'inline inventory'
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'glazed'
|
|
7
|
+
require 'glazed/must'
|
|
8
|
+
require 'glazed/proto'
|
|
9
|
+
require 'glazed/sources'
|
|
10
|
+
require 'glazed/sources/base'
|
|
11
|
+
|
|
12
|
+
module Glazed
|
|
13
|
+
module Sources
|
|
14
|
+
# Inventory read from ProtoJSON files of {Proto::V1::InventoryFile}.
|
|
15
|
+
#
|
|
16
|
+
# Each file's `defaults` fill fields its nodes leave unset. The `params`
|
|
17
|
+
# and `secrets` maps are merged key by key.
|
|
18
|
+
#
|
|
19
|
+
# @example
|
|
20
|
+
# Glazed::Sources::JsonFiles.new(paths: ['inventory/*.json']).nodes
|
|
21
|
+
class JsonFiles < Base
|
|
22
|
+
# Raised when a file is not a valid inventory file.
|
|
23
|
+
InvalidFile = Base::InvalidInventory
|
|
24
|
+
|
|
25
|
+
# @param paths [Array<String>] file paths or glob patterns
|
|
26
|
+
#: (paths: Array[String]) -> void
|
|
27
|
+
def initialize(paths:)
|
|
28
|
+
super()
|
|
29
|
+
@paths = paths #: Array[String]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Builds a provider from {Proto::V1::JsonFilesInventory}.
|
|
33
|
+
#
|
|
34
|
+
# @param settings [Google::Protobuf::MessageExts] a {Proto::V1::JsonFilesInventory}
|
|
35
|
+
# @return [JsonFiles] the provider
|
|
36
|
+
# @raise [Must::Violation] when settings is another message
|
|
37
|
+
#: (Google::Protobuf::MessageExts settings) -> JsonFiles
|
|
38
|
+
def self.from_proto(settings)
|
|
39
|
+
new(paths: Must.be_a(settings, Proto::V1::JsonFilesInventory).paths.to_a)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# @return [Array<String>] file paths or glob patterns
|
|
43
|
+
#: Array[String]
|
|
44
|
+
attr_reader :paths
|
|
45
|
+
|
|
46
|
+
# Reads every file and returns its nodes.
|
|
47
|
+
#
|
|
48
|
+
# @return [Array<Proto::V1::Node>] nodes in file order
|
|
49
|
+
# @raise [InvalidFile] when a file is malformed or a node lacks a name or dialect
|
|
50
|
+
# @raise [Sources::DuplicateNode] when a node name appears twice
|
|
51
|
+
# @raise [Errno::ENOENT] when a literal path does not exist
|
|
52
|
+
#: () -> Array[Proto::V1::Node]
|
|
53
|
+
def nodes
|
|
54
|
+
origins = {} #: Hash[String, String]
|
|
55
|
+
files.flat_map do |path|
|
|
56
|
+
load_file(path).each do |node|
|
|
57
|
+
if (other = origins[node.name])
|
|
58
|
+
raise DuplicateNode, "node #{node.name.inspect} is defined in both #{other} and #{path}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
origins[node.name] = path
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @return [String] the path patterns
|
|
67
|
+
#: () -> String
|
|
68
|
+
def description = "json_files #{paths.join(', ')}"
|
|
69
|
+
|
|
70
|
+
# @return [Array<String>] files matched by {#paths}, in a stable order
|
|
71
|
+
#: () -> Array[String]
|
|
72
|
+
private def files
|
|
73
|
+
paths.flat_map do |pattern|
|
|
74
|
+
matches = Dir.glob(pattern).sort
|
|
75
|
+
matches.empty? && !pattern.match?(/[*?\[{]/) ? [pattern] : matches
|
|
76
|
+
end.uniq
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @param path [String] inventory file
|
|
80
|
+
# @return [Array<Proto::V1::Node>] nodes with defaults applied
|
|
81
|
+
# @raise [InvalidFile] when the file is malformed
|
|
82
|
+
# @raise [InvalidMessage] when the file breaks the rules of its schema
|
|
83
|
+
#: (String path) -> Array[Proto::V1::Node]
|
|
84
|
+
private def load_file(path) = nodes_from_json(File.read(path), origin: path)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'glazed'
|
|
7
|
+
require 'glazed/finder'
|
|
8
|
+
require 'glazed/must'
|
|
9
|
+
require 'glazed/plugin_registry'
|
|
10
|
+
require 'glazed/proto'
|
|
11
|
+
require 'glazed/sources/base'
|
|
12
|
+
require 'glazed/validation'
|
|
13
|
+
|
|
14
|
+
module Glazed
|
|
15
|
+
# Namespace of inventory providers, looked up by name.
|
|
16
|
+
module Sources
|
|
17
|
+
# Raised when an inventory source names no provider.
|
|
18
|
+
class MissingProvider < Glazed::Error; end
|
|
19
|
+
# Raised when two nodes share a name.
|
|
20
|
+
class DuplicateNode < Glazed::Error; end
|
|
21
|
+
|
|
22
|
+
# Finds a provider class by snake_case name.
|
|
23
|
+
#
|
|
24
|
+
# @param name [String, Symbol] provider name, such as "json_files"
|
|
25
|
+
# @return [Class<Base>] the provider class
|
|
26
|
+
# @raise [Finder::NotFound] when there is none, for example when the gem providing it is not installed
|
|
27
|
+
# @raise [Must::Violation] when the name resolves to something other than a provider
|
|
28
|
+
#: (String | Symbol name) -> singleton(Base)
|
|
29
|
+
def self.find(name)
|
|
30
|
+
found = Finder.find(self, 'glazed/sources', name)
|
|
31
|
+
return found if found.is_a?(Class) && found < Base
|
|
32
|
+
|
|
33
|
+
raise Must::Violation, "#{found} is not an inventory provider"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Inventory providers implemented by plugins, by settings message type.
|
|
37
|
+
PLUGINS = PluginRegistry.new('inventory provider') #: PluginRegistry
|
|
38
|
+
|
|
39
|
+
# Registers a plugin provider, selected by `plugin` sources whose settings are of the given type.
|
|
40
|
+
#
|
|
41
|
+
# @param settings_class [Class] generated message class of the provider settings
|
|
42
|
+
# @param implementation [Class<Base>] provider built with {Base.from_proto}
|
|
43
|
+
# @return [void]
|
|
44
|
+
# @example
|
|
45
|
+
# Glazed::Sources.register(GlazedNetbox::V1::NetboxInventory, GlazedNetbox::Source)
|
|
46
|
+
#: (singleton(Google::Protobuf::AbstractMessage) settings_class, singleton(Base) implementation) -> void
|
|
47
|
+
def self.register(settings_class, implementation)
|
|
48
|
+
PLUGINS.register(settings_class, implementation)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Builds the provider a source selects.
|
|
52
|
+
#
|
|
53
|
+
# @param source [Proto::V1::InventorySource] provider settings
|
|
54
|
+
# @return [Base] the provider
|
|
55
|
+
# @raise [InvalidMessage] when the source breaks the rules of its schema
|
|
56
|
+
# @raise [Finder::NotFound] when the provider is not available
|
|
57
|
+
# @raise [PluginRegistry::UnknownPlugin] when no plugin is registered for the settings type
|
|
58
|
+
#: (Proto::V1::InventorySource source) -> Base
|
|
59
|
+
def self.build(source)
|
|
60
|
+
Validation.validate!(source)
|
|
61
|
+
provider = source.provider or raise MissingProvider, 'inventory source sets no provider'
|
|
62
|
+
if (packed = source.plugin)
|
|
63
|
+
implementation, settings = PLUGINS.resolve(packed)
|
|
64
|
+
return provider_class(implementation).from_proto(settings)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
settings = Must.be_a(source[provider.to_s], Google::Protobuf::MessageExts)
|
|
68
|
+
find(provider).from_proto(settings)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param implementation [Module] registered implementation
|
|
72
|
+
# @return [Class<Base>] the implementation as a provider class
|
|
73
|
+
# @raise [Must::Violation] when it is not a provider
|
|
74
|
+
#: (Module[untyped] implementation) -> singleton(Base)
|
|
75
|
+
private_class_method def self.provider_class(implementation)
|
|
76
|
+
return implementation if implementation.is_a?(Class) && implementation < Base
|
|
77
|
+
|
|
78
|
+
raise Must::Violation, "#{implementation} is not an inventory provider"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Reads the nodes of every source.
|
|
82
|
+
#
|
|
83
|
+
# @param sources [Array<Proto::V1::InventorySource>] sources in order
|
|
84
|
+
# @return [Array<Proto::V1::Node>] nodes in source order
|
|
85
|
+
# @raise [DuplicateNode] when a node name appears twice
|
|
86
|
+
# @raise [Base::InvalidInventory] when a node lacks a name or dialect
|
|
87
|
+
# @raise [InvalidMessage] when a source breaks the rules of its schema
|
|
88
|
+
#: (Array[Proto::V1::InventorySource] sources) -> Array[Proto::V1::Node]
|
|
89
|
+
def self.nodes(sources)
|
|
90
|
+
origins = {} #: Hash[String, String]
|
|
91
|
+
sources.flat_map do |source|
|
|
92
|
+
provider = build(source)
|
|
93
|
+
provider.nodes.each do |node|
|
|
94
|
+
raise Base::InvalidInventory, "#{provider.description}: a node has no name" if node.name.empty?
|
|
95
|
+
raise Base::InvalidInventory, "#{provider.description}: node #{node.name.inspect} has no dialect" if node.dialect.empty?
|
|
96
|
+
if (other = origins[node.name])
|
|
97
|
+
raise DuplicateNode, "node #{node.name.inspect} is defined in both #{other} and #{provider.description}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
origins[node.name] = provider.description
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
|
|
6
|
+
require 'google/protobuf'
|
|
7
|
+
require 'glazed'
|
|
8
|
+
require 'glazed/proto'
|
|
9
|
+
|
|
10
|
+
module Glazed
|
|
11
|
+
module StateBackends
|
|
12
|
+
# Store of {Proto::V1::NodeSchedule} records, one per workflow and node.
|
|
13
|
+
# Records change only through read-modify-write blocks, which a backend may
|
|
14
|
+
# run more than once when another process changed the records meanwhile,
|
|
15
|
+
# so a block must compute its result from the record it is given alone.
|
|
16
|
+
# Subclasses implement {.from_proto}, {#update_all}, {#list_due} and {#list}.
|
|
17
|
+
class Base
|
|
18
|
+
#: type updater = ^(String node, Proto::V1::NodeSchedule? record) -> Proto::V1::NodeSchedule?
|
|
19
|
+
|
|
20
|
+
# Builds a state backend from its settings message.
|
|
21
|
+
#
|
|
22
|
+
# @param settings [Google::Protobuf::MessageExts] backend settings from {Proto::V1::StateBackendConfig}
|
|
23
|
+
# @param context [StepContext] configured storages, and where progress is reported
|
|
24
|
+
# @return [Base] the state backend
|
|
25
|
+
#: (Google::Protobuf::MessageExts settings, context: StepContext) -> Base
|
|
26
|
+
def self.from_proto(settings, context:) = raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
# Replaces the records of nodes with what the block returns.
|
|
29
|
+
#
|
|
30
|
+
# @param workflow [String] workflow name
|
|
31
|
+
# @param nodes [Array<String>] node names
|
|
32
|
+
# @yieldparam node [String] node name
|
|
33
|
+
# @yieldparam record [Proto::V1::NodeSchedule, nil] the current record, or nil when there is none
|
|
34
|
+
# @yieldreturn [Proto::V1::NodeSchedule, nil] the new record, or nil to delete it
|
|
35
|
+
# @return [Hash{String => Proto::V1::NodeSchedule}] records held afterwards, by node name
|
|
36
|
+
#: (String workflow, Array[String] nodes) { (String node, Proto::V1::NodeSchedule? record) -> Proto::V1::NodeSchedule? } -> Hash[String, Proto::V1::NodeSchedule]
|
|
37
|
+
def update_all(workflow, nodes, &) = raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
# Replaces the record of one node with what the block returns.
|
|
40
|
+
#
|
|
41
|
+
# @param workflow [String] workflow name
|
|
42
|
+
# @param node [String] node name
|
|
43
|
+
# @yieldparam record [Proto::V1::NodeSchedule, nil] the current record, or nil when there is none
|
|
44
|
+
# @yieldreturn [Proto::V1::NodeSchedule, nil] the new record, or nil to delete it
|
|
45
|
+
# @return [Proto::V1::NodeSchedule, nil] the record held afterwards
|
|
46
|
+
#: (String workflow, String node) { (Proto::V1::NodeSchedule? record) -> Proto::V1::NodeSchedule? } -> Proto::V1::NodeSchedule?
|
|
47
|
+
def update(workflow, node, &block)
|
|
48
|
+
update_all(workflow, [node]) { |_node, record| block.call(record) }[node]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Records due at or before a time and not leased to an orchestrator.
|
|
52
|
+
#
|
|
53
|
+
# @param workflow [String] workflow name
|
|
54
|
+
# @param now [Time] the time; a lease ending at or before it has expired
|
|
55
|
+
# @param limit [Integer, nil] records returned at most; all when nil
|
|
56
|
+
# @return [Array<Proto::V1::NodeSchedule>] the records, earliest due first
|
|
57
|
+
#: (String workflow, now: Time, ?limit: Integer?) -> Array[Proto::V1::NodeSchedule]
|
|
58
|
+
def list_due(workflow, now:, limit: nil) = raise NotImplementedError
|
|
59
|
+
|
|
60
|
+
# @param workflow [String] workflow name
|
|
61
|
+
# @return [Array<Proto::V1::NodeSchedule>] every record of the workflow, by node name
|
|
62
|
+
#: (String workflow) -> Array[Proto::V1::NodeSchedule]
|
|
63
|
+
def list(workflow) = raise NotImplementedError
|
|
64
|
+
|
|
65
|
+
# Releases resources held by the backend.
|
|
66
|
+
#
|
|
67
|
+
# @return [void]
|
|
68
|
+
#: () -> void
|
|
69
|
+
def close; end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|