netstack 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -3
- data/{LICENSE.txt → LICENSE} +7 -6
- data/NOTICE +25 -0
- data/README.md +58 -20
- data/docs/acceptance.md +92 -0
- data/docs/backups.md +100 -0
- data/docs/configuration.md +198 -0
- data/docs/connector.md +187 -0
- data/docs/ddi.md +142 -0
- data/docs/deployment.md +47 -0
- data/docs/foundations.md +45 -0
- data/docs/implementation.md +68 -0
- data/docs/inventory.md +42 -0
- data/docs/monitoring.md +227 -0
- data/docs/motor_integration.md +96 -0
- data/docs/performance.md +39 -0
- data/docs/release.md +20 -0
- data/docs/vxlan.md +91 -0
- data/docs/wireless.md +81 -0
- data/lib/netstack/backups/artifact.rb +23 -0
- data/lib/netstack/backups/diff.rb +15 -0
- data/lib/netstack/backups/repository.rb +77 -0
- data/lib/netstack/backups/request.rb +78 -0
- data/lib/netstack/circuits/business_calendar.rb +51 -0
- data/lib/netstack/circuits/capacity_review.rb +191 -0
- data/lib/netstack/circuits/endpoint.rb +25 -0
- data/lib/netstack/circuits/monitoring_plan.rb +212 -0
- data/lib/netstack/configuration/artifact.rb +85 -0
- data/lib/netstack/configuration/artifact_resolver.rb +30 -0
- data/lib/netstack/configuration/bootstrap/payload.rb +67 -0
- data/lib/netstack/configuration/bootstrap/validator.rb +224 -0
- data/lib/netstack/configuration/catalog.rb +60 -0
- data/lib/netstack/configuration/generator.rb +87 -0
- data/lib/netstack/configuration/input_contract.rb +82 -0
- data/lib/netstack/configuration/payload.rb +168 -0
- data/lib/netstack/configuration/reader.rb +124 -0
- data/lib/netstack/configuration/support.rb +30 -0
- data/lib/netstack/configuration/system_configuration.rb +56 -0
- data/lib/netstack/configuration/template_manager.rb +174 -0
- data/lib/netstack/configuration/template_store.rb +40 -0
- data/lib/netstack/configuration/validator.rb +566 -0
- data/lib/netstack/connector/authentication.rb +77 -0
- data/lib/netstack/connector/authentication_error.rb +11 -0
- data/lib/netstack/connector/backup.rb +19 -0
- data/lib/netstack/connector/backup_busy.rb +11 -0
- data/lib/netstack/connector/backup_persistence_error.rb +32 -0
- data/lib/netstack/connector/command.rb +63 -0
- data/lib/netstack/connector/command_result.rb +24 -0
- data/lib/netstack/connector/command_timeout.rb +11 -0
- data/lib/netstack/connector/configuration.rb +138 -0
- data/lib/netstack/connector/connection_closed.rb +11 -0
- data/lib/netstack/connector/connection_error.rb +11 -0
- data/lib/netstack/connector/device.rb +284 -0
- data/lib/netstack/connector/device_error.rb +11 -0
- data/lib/netstack/connector/dialogue.rb +55 -0
- data/lib/netstack/connector/error.rb +44 -0
- data/lib/netstack/connector/error_metadata.rb +58 -0
- data/lib/netstack/connector/event.rb +24 -0
- data/lib/netstack/connector/execution.rb +110 -0
- data/lib/netstack/connector/interaction.rb +55 -0
- data/lib/netstack/connector/internal_error.rb +11 -0
- data/lib/netstack/connector/known_hosts.rb +128 -0
- data/lib/netstack/connector/local_backup.rb +63 -0
- data/lib/netstack/connector/log/event.rb +65 -0
- data/lib/netstack/connector/log/formatter.rb +18 -0
- data/lib/netstack/connector/log/messages.rb +54 -0
- data/lib/netstack/connector/log/redacting_writer.rb +33 -0
- data/lib/netstack/connector/log/transcript.rb +35 -0
- data/lib/netstack/connector/log.rb +262 -0
- data/lib/netstack/connector/log_error.rb +11 -0
- data/lib/netstack/connector/login_timeout.rb +11 -0
- data/lib/netstack/connector/output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/parsing_error.rb +11 -0
- data/lib/netstack/connector/profile/builder.rb +238 -0
- data/lib/netstack/connector/profile.rb +225 -0
- data/lib/netstack/connector/prompt_error.rb +11 -0
- data/lib/netstack/connector/recovery.rb +59 -0
- data/lib/netstack/connector/redactor.rb +100 -0
- data/lib/netstack/connector/response.rb +25 -0
- data/lib/netstack/connector/response_reader.rb +140 -0
- data/lib/netstack/connector/result.rb +50 -0
- data/lib/netstack/connector/running_config/rendered.rb +16 -0
- data/lib/netstack/connector/running_config/strategy.rb +28 -0
- data/lib/netstack/connector/running_config.rb +113 -0
- data/lib/netstack/connector/save_config.rb +23 -0
- data/lib/netstack/connector/script.rb +59 -0
- data/lib/netstack/connector/script_error.rb +11 -0
- data/lib/netstack/connector/script_output_limit_exceeded.rb +11 -0
- data/lib/netstack/connector/session.rb +414 -0
- data/lib/netstack/connector/session_busy.rb +11 -0
- data/lib/netstack/connector/storage/backup_lock.rb +117 -0
- data/lib/netstack/connector/storage/private_file.rb +111 -0
- data/lib/netstack/connector/storage/safe_file.rb +64 -0
- data/lib/netstack/connector/storage/saved_config.rb +76 -0
- data/lib/netstack/connector/terminal_renderer.rb +186 -0
- data/lib/netstack/connector/terminal_text.rb +32 -0
- data/lib/netstack/connector/textfsm.rb +71 -0
- data/lib/netstack/connector/tftp/file_upload.rb +53 -0
- data/lib/netstack/connector/tftp/strategy.rb +67 -0
- data/lib/netstack/connector/tftp.rb +116 -0
- data/lib/netstack/connector/tftp_completion_error.rb +36 -0
- data/lib/netstack/connector/tftp_receipt.rb +54 -0
- data/lib/netstack/connector/tftp_target.rb +65 -0
- data/lib/netstack/connector/topology/checkpoint_error.rb +16 -0
- data/lib/netstack/connector/topology/deployment_adapter.rb +163 -0
- data/lib/netstack/connector/topology/execution_result.rb +34 -0
- data/lib/netstack/connector/topology/immediate_strategy.rb +39 -0
- data/lib/netstack/connector/topology/interface_description.rb +46 -0
- data/lib/netstack/connector/topology/interface_name.rb +60 -0
- data/lib/netstack/connector/topology/plan.rb +105 -0
- data/lib/netstack/connector/topology/strategy.rb +76 -0
- data/lib/netstack/connector/topology.rb +255 -0
- data/lib/netstack/connector/transport_error.rb +11 -0
- data/lib/netstack/connector/transports.rb +201 -0
- data/lib/netstack/connector/underlying_error.rb +25 -0
- data/lib/netstack/connector/unsupported_operation.rb +11 -0
- data/lib/netstack/connector/vendors/cisco_ios/running_config.rb +22 -0
- data/lib/netstack/connector/vendors/cisco_ios/tftp_backup.rb +41 -0
- data/lib/netstack/connector/vendors/cisco_ios/topology.rb +54 -0
- data/lib/netstack/connector/vendors/cisco_ios.rb +44 -0
- data/lib/netstack/connector/vendors/cisco_nxos/running_config.rb +25 -0
- data/lib/netstack/connector/vendors/cisco_nxos/tftp_backup.rb +43 -0
- data/lib/netstack/connector/vendors/cisco_nxos.rb +51 -0
- data/lib/netstack/connector/vendors/h3c/tftp_backup.rb +34 -0
- data/lib/netstack/connector/vendors/h3c/topology.rb +72 -0
- data/lib/netstack/connector/vendors/h3c.rb +75 -0
- data/lib/netstack/connector/vendors/h3c_wireless.rb +13 -0
- data/lib/netstack/connector/vendors/hillstone/running_config.rb +16 -0
- data/lib/netstack/connector/vendors/hillstone/tftp_backup.rb +55 -0
- data/lib/netstack/connector/vendors/hillstone/topology.rb +64 -0
- data/lib/netstack/connector/vendors/hillstone.rb +39 -0
- data/lib/netstack/connector/vendors/huawei/tftp_backup.rb +26 -0
- data/lib/netstack/connector/vendors/huawei.rb +62 -0
- data/lib/netstack/connector/vendors/palo_alto/running_config.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/palo_alto/topology.rb +77 -0
- data/lib/netstack/connector/vendors/palo_alto.rb +39 -0
- data/lib/netstack/connector/vendors/radware/running_config.rb +21 -0
- data/lib/netstack/connector/vendors/radware/tftp_backup.rb +49 -0
- data/lib/netstack/connector/vendors/radware/topology.rb +19 -0
- data/lib/netstack/connector/vendors/radware.rb +59 -0
- data/lib/netstack/connector/write_timeout.rb +11 -0
- data/lib/netstack/connector.rb +32 -0
- data/lib/netstack/ddi/applier.rb +107 -0
- data/lib/netstack/ddi/deployment.rb +137 -0
- data/lib/netstack/ddi/inputs.rb +61 -0
- data/lib/netstack/ddi/manager.rb +197 -0
- data/lib/netstack/ddi/plan.rb +99 -0
- data/lib/netstack/ddi/scope.rb +127 -0
- data/lib/netstack/ddi/settings.rb +44 -0
- data/lib/netstack/ddi/state.rb +166 -0
- data/lib/netstack/deployment/checkpoint_error.rb +14 -0
- data/lib/netstack/deployment/cli_adapter.rb +64 -0
- data/lib/netstack/deployment/executor.rb +131 -0
- data/lib/netstack/deployment/plan.rb +82 -0
- data/lib/netstack/deployment/receipt.rb +78 -0
- data/lib/netstack/deployment/recovery.rb +19 -0
- data/lib/netstack/deployment/step.rb +16 -0
- data/lib/netstack/dimension.rb +30 -0
- data/lib/netstack/error.rb +14 -0
- data/lib/netstack/input_error.rb +9 -0
- data/lib/netstack/integrations/bluecat/client.rb +84 -0
- data/lib/netstack/integrations/http/client.rb +146 -0
- data/lib/netstack/integrations/http/error.rb +10 -0
- data/lib/netstack/integrations/http/response.rb +28 -0
- data/lib/netstack/integrations/http_delivery/adapter.rb +51 -0
- data/lib/netstack/integrations/http_delivery/client.rb +127 -0
- data/lib/netstack/integrations/http_delivery/result.rb +27 -0
- data/lib/netstack/integrations/oxidized/client.rb +89 -0
- data/lib/netstack/integrations/zabbix/client.rb +211 -0
- data/lib/netstack/integrations/zabbix/error.rb +10 -0
- data/lib/netstack/inventory/address.rb +30 -0
- data/lib/netstack/inventory/changes.rb +13 -0
- data/lib/netstack/inventory/cli_parser.rb +75 -0
- data/lib/netstack/inventory/collector.rb +99 -0
- data/lib/netstack/inventory/device.rb +14 -0
- data/lib/netstack/inventory/interface.rb +25 -0
- data/lib/netstack/inventory/neighbor.rb +15 -0
- data/lib/netstack/inventory/reconciler.rb +74 -0
- data/lib/netstack/inventory/snapshot.rb +69 -0
- data/lib/netstack/inventory/transceiver.rb +23 -0
- data/lib/netstack/ipv4.rb +204 -0
- data/lib/netstack/ipv6.rb +88 -0
- data/lib/netstack/monitoring/applier.rb +179 -0
- data/lib/netstack/monitoring/device_plan.rb +23 -0
- data/lib/netstack/monitoring/host_plan.rb +152 -0
- data/lib/netstack/monitoring/inputs.rb +73 -0
- data/lib/netstack/monitoring/measurements.rb +89 -0
- data/lib/netstack/monitoring/mutation.rb +37 -0
- data/lib/netstack/monitoring/plan.rb +69 -0
- data/lib/netstack/monitoring/probe_plan.rb +19 -0
- data/lib/netstack/monitoring/state_comparison.rb +37 -0
- data/lib/netstack/range_set.rb +181 -0
- data/lib/netstack/routing/lookup.rb +46 -0
- data/lib/netstack/routing/lookup_result.rb +14 -0
- data/lib/netstack/routing/route.rb +32 -0
- data/lib/netstack/routing/snapshot.rb +23 -0
- data/lib/netstack/target.rb +15 -0
- data/lib/netstack/unsupported_capability.rb +9 -0
- data/lib/netstack/values.rb +70 -0
- data/lib/netstack/version.rb +1 -1
- data/lib/netstack/vxlan/auditor.rb +187 -0
- data/lib/netstack/vxlan/cisco_nxos/config_parser.rb +327 -0
- data/lib/netstack/vxlan/cisco_nxos/deployment_adapter.rb +63 -0
- data/lib/netstack/vxlan/cisco_nxos/runtime_parser.rb +182 -0
- data/lib/netstack/vxlan/cisco_nxos/segment_renderer.rb +42 -0
- data/lib/netstack/vxlan/collector.rb +77 -0
- data/lib/netstack/vxlan/fabric.rb +29 -0
- data/lib/netstack/vxlan/finding.rb +20 -0
- data/lib/netstack/vxlan/node.rb +51 -0
- data/lib/netstack/vxlan/route_policy.rb +57 -0
- data/lib/netstack/vxlan/runtime_result.rb +24 -0
- data/lib/netstack/vxlan/segment.rb +35 -0
- data/lib/netstack/vxlan/segment_plan.rb +25 -0
- data/lib/netstack/vxlan/segment_planner.rb +156 -0
- data/lib/netstack/vxlan/snapshot.rb +34 -0
- data/lib/netstack/vxlan/validation.rb +58 -0
- data/lib/netstack/vxlan/verifier.rb +52 -0
- data/lib/netstack/vxlan/vpc_domain.rb +20 -0
- data/lib/netstack/wireless/access_point.rb +12 -0
- data/lib/netstack/wireless/ap_group.rb +12 -0
- data/lib/netstack/wireless/assessment.rb +13 -0
- data/lib/netstack/wireless/assessor.rb +83 -0
- data/lib/netstack/wireless/authentication_connection.rb +12 -0
- data/lib/netstack/wireless/bss.rb +12 -0
- data/lib/netstack/wireless/client.rb +12 -0
- data/lib/netstack/wireless/collector.rb +191 -0
- data/lib/netstack/wireless/configuration.rb +13 -0
- data/lib/netstack/wireless/finding.rb +12 -0
- data/lib/netstack/wireless/h3c/config_parser.rb +229 -0
- data/lib/netstack/wireless/h3c/lldp_parser.rb +69 -0
- data/lib/netstack/wireless/h3c/runtime_parser.rb +240 -0
- data/lib/netstack/wireless/huawei/runtime_parser.rb +83 -0
- data/lib/netstack/wireless/invalid_snapshot.rb +11 -0
- data/lib/netstack/wireless/neighbor.rb +12 -0
- data/lib/netstack/wireless/normalizer.rb +90 -0
- data/lib/netstack/wireless/parse_result.rb +13 -0
- data/lib/netstack/wireless/radio.rb +12 -0
- data/lib/netstack/wireless/reconciler.rb +115 -0
- data/lib/netstack/wireless/reconciliation.rb +11 -0
- data/lib/netstack/wireless/security_policy.rb +12 -0
- data/lib/netstack/wireless/service_template.rb +12 -0
- data/lib/netstack/wireless/service_template_bindings.rb +56 -0
- data/lib/netstack/wireless/snapshot.rb +150 -0
- data/lib/netstack/wireless/state.rb +23 -0
- data/lib/netstack/wireless/view.rb +154 -0
- data/lib/netstack.rb +10 -3
- data/resources/configuration/cisco-bootstrap.erb +118 -0
- data/resources/configuration/cisco-distributed-gateway.erb +40 -0
- data/resources/configuration/cisco-n9k-vpc.erb +62 -0
- data/resources/configuration/cisco-server-port.erb +43 -0
- data/resources/configuration/h3c-bootstrap.erb +108 -0
- data/resources/configuration/h3c-irf.erb +57 -0
- data/resources/configuration/h3c-server-port.erb +36 -0
- data/resources/configuration/hillstone-ipsec-vpn.erb +68 -0
- data/resources/configuration/hillstone-policy.erb +31 -0
- data/resources/configuration/paloalto-ipsec-vpn.erb +39 -0
- data/resources/configuration/radware-load-balancer.erb +26 -0
- data/resources/textfsm/cisco_cdp_neighbors_detail.textfsm +7 -0
- data/resources/textfsm/cisco_ios_running_config_interfaces.textfsm +12 -0
- data/resources/textfsm/cisco_ios_show_ip_interface_brief.textfsm +7 -0
- data/resources/textfsm/h3c_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/h3c_lldp_local_first.textfsm +8 -0
- data/resources/textfsm/h3c_lldp_name_first.textfsm +8 -0
- data/resources/textfsm/hillstone_interface_descriptions.textfsm +11 -0
- data/resources/textfsm/hillstone_lldp_neighbors.textfsm +7 -0
- data/resources/textfsm/index +8 -0
- data/resources/textfsm/palo_alto_interface_descriptions.textfsm +6 -0
- data/resources/textfsm/palo_alto_lldp_neighbors.textfsm +11 -0
- data/resources/textfsm/radware_port_names.textfsm +13 -0
- metadata +588 -27
- data/.rubocop.yml +0 -13
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -10
- data/Gemfile.lock +0 -40
- data/Rakefile +0 -8
- data/bin/console +0 -15
- data/bin/setup +0 -8
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Netstack
|
|
7
|
+
module Deployment
|
|
8
|
+
# 可持久化的单目标有序意图;规范序列化产生稳定摘要供恢复核验。
|
|
9
|
+
# 配置正文应放在私有工件中,参数与元数据只保留引用和安全摘要。
|
|
10
|
+
class Plan < Data.define(:target, :steps, :preconditions, :metadata)
|
|
11
|
+
SCHEMA_VERSION = 1
|
|
12
|
+
|
|
13
|
+
def initialize(target:, steps:, preconditions: {}, metadata: {})
|
|
14
|
+
raise InputError, "plan target must be a Target" unless target.is_a?(Target)
|
|
15
|
+
unless steps.is_a?(Array) && steps.all? { |step| step.is_a?(Step) } && steps.map(&:id).uniq.size == steps.size
|
|
16
|
+
raise InputError, "plan steps must have unique identities"
|
|
17
|
+
end
|
|
18
|
+
raise InputError, "plan metadata must be hashes" unless preconditions.is_a?(Hash) && metadata.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
super(target: target, steps: steps.dup.freeze, preconditions: Values.copy(preconditions), metadata: Values.copy(metadata))
|
|
21
|
+
# 适配器执行预检读取或写入之前,先验证序列化契约。
|
|
22
|
+
payload
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def payload
|
|
26
|
+
canonical({ schema_version: SCHEMA_VERSION, target: target.to_h, steps: steps.map(&:to_h), preconditions: preconditions, metadata: metadata })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def digest = Digest::SHA256.hexdigest(JSON.generate(payload))
|
|
30
|
+
|
|
31
|
+
def self.from_h(value)
|
|
32
|
+
raise InputError, "plan must be an object" unless value.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
validate_serialized_keys!(value)
|
|
35
|
+
data = JSON.parse(JSON.generate(value), symbolize_names: true)
|
|
36
|
+
raise InputError, "unsupported plan schema" unless data.fetch(:schema_version) == SCHEMA_VERSION
|
|
37
|
+
|
|
38
|
+
new(target: Target.new(**data.fetch(:target)), steps: data.fetch(:steps).map { |step| Step.new(**step) },
|
|
39
|
+
preconditions: data.fetch(:preconditions), metadata: data.fetch(:metadata))
|
|
40
|
+
rescue KeyError, TypeError, NoMethodError, JSON::GeneratorError, JSON::ParserError, ArgumentError
|
|
41
|
+
raise InputError, "invalid plan payload"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.validate_serialized_keys!(value)
|
|
45
|
+
case value
|
|
46
|
+
when Hash
|
|
47
|
+
raise InputError, "plan keys must be strings or symbols" unless value.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
|
|
48
|
+
|
|
49
|
+
keys = value.keys.map(&:to_s)
|
|
50
|
+
raise InputError, "duplicate serialized plan keys" unless keys.uniq.size == keys.size
|
|
51
|
+
|
|
52
|
+
value.each_value { |item| validate_serialized_keys!(item) }
|
|
53
|
+
when Array then value.each { |item| validate_serialized_keys!(item) }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
private_class_method :validate_serialized_keys!
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def canonical(value)
|
|
61
|
+
case value
|
|
62
|
+
when Hash
|
|
63
|
+
raise InputError, "plan keys must be strings or symbols" unless value.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) }
|
|
64
|
+
|
|
65
|
+
keys = value.keys.map(&:to_s)
|
|
66
|
+
raise InputError, "duplicate serialized plan keys" unless keys.uniq.size == keys.size
|
|
67
|
+
|
|
68
|
+
value.to_h { |key, item| [key.to_s, canonical(item)] }.sort.to_h
|
|
69
|
+
when Array then value.map { |item| canonical(item) }
|
|
70
|
+
when Time then value.getutc.iso8601(6)
|
|
71
|
+
when Symbol then value.to_s
|
|
72
|
+
when Float
|
|
73
|
+
raise InputError, "plan numbers must be finite" unless value.finite?
|
|
74
|
+
|
|
75
|
+
value
|
|
76
|
+
when String, Integer, TrueClass, FalseClass, NilClass then value
|
|
77
|
+
else raise InputError, "plan contains a non-serializable value"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Deployment
|
|
7
|
+
# 不可变的执行前缀;证据只应包含安全的身份与摘要,
|
|
8
|
+
# 不能包含凭据、配置正文或传输原文。
|
|
9
|
+
class Receipt < Data.define(:execution_id, :plan_digest, :target_key, :steps, :planned_step_ids)
|
|
10
|
+
STATES = %i[started applied accepted verified failed unknown not_applied].freeze
|
|
11
|
+
SCHEMA_VERSION = 1
|
|
12
|
+
STEP_FIELDS = %i[step_id status idempotency_key remote_id error_code evidence].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(execution_id:, plan_digest:, target_key:, planned_step_ids:, steps: [])
|
|
15
|
+
raise InputError, "receipt steps must be an Array" unless steps.is_a?(Array)
|
|
16
|
+
unless planned_step_ids.is_a?(Array) && planned_step_ids.all? { |id| id.is_a?(String) && !id.empty? } && planned_step_ids.uniq == planned_step_ids
|
|
17
|
+
raise InputError, "planned step identities must be unique strings"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
records = steps.map do |step|
|
|
21
|
+
raise InputError, "receipt step must be a Hash" unless step.is_a?(Hash)
|
|
22
|
+
|
|
23
|
+
self.class.send(:validate_serialized_keys!, step)
|
|
24
|
+
item = step.transform_keys(&:to_sym)
|
|
25
|
+
raise InputError, "unknown receipt step fields" unless (item.keys - STEP_FIELDS).empty?
|
|
26
|
+
item[:step_id] = Values.text(item.fetch(:step_id), name: "receipt step id")
|
|
27
|
+
item[:idempotency_key] = Values.text(item[:idempotency_key], name: "idempotency key") if item.key?(:idempotency_key)
|
|
28
|
+
item[:remote_id] = Values.text(item[:remote_id], name: "remote id") if item.key?(:remote_id)
|
|
29
|
+
state = item.fetch(:status).to_sym
|
|
30
|
+
raise InputError, "invalid receipt status" unless STATES.include?(state)
|
|
31
|
+
item[:error_code] = item[:error_code].to_sym if item[:error_code]
|
|
32
|
+
|
|
33
|
+
Values.copy(item.merge(status: state))
|
|
34
|
+
end
|
|
35
|
+
raise InputError, "duplicate receipt steps" unless records.map { |step| step[:step_id] }.uniq.size == records.size
|
|
36
|
+
unless records.map { |step| step[:step_id] } == planned_step_ids.first(records.size)
|
|
37
|
+
raise InputError, "receipt steps must follow plan order"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
super(execution_id: Values.text(execution_id, name: "execution id"), plan_digest: Values.text(plan_digest, name: "plan digest"),
|
|
41
|
+
target_key: Values.text(target_key, name: "target key"), steps: records.freeze, planned_step_ids: Values.copy(planned_step_ids))
|
|
42
|
+
rescue KeyError, NoMethodError
|
|
43
|
+
raise InputError, "invalid receipt step"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def verified? = !steps.empty? && steps.size == planned_step_ids.size && steps.all? { |step| step[:status] == :verified }
|
|
47
|
+
def no_changes? = planned_step_ids.empty?
|
|
48
|
+
def unknown? = steps.any? { |step| %i[started unknown applied accepted].include?(step[:status]) }
|
|
49
|
+
def failed? = steps.any? { |step| %i[failed not_applied].include?(step[:status]) }
|
|
50
|
+
|
|
51
|
+
def payload = { schema_version: SCHEMA_VERSION, **to_h }
|
|
52
|
+
|
|
53
|
+
def self.from_h(value)
|
|
54
|
+
raise InputError, "receipt must be an object" unless value.is_a?(Hash)
|
|
55
|
+
|
|
56
|
+
validate_serialized_keys!(value)
|
|
57
|
+
data = JSON.parse(JSON.generate(value), symbolize_names: true)
|
|
58
|
+
raise InputError, "unsupported receipt schema" unless data.delete(:schema_version) == SCHEMA_VERSION
|
|
59
|
+
|
|
60
|
+
new(**data)
|
|
61
|
+
rescue JSON::GeneratorError, JSON::ParserError, ArgumentError, TypeError
|
|
62
|
+
raise InputError, "invalid receipt payload"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.validate_serialized_keys!(value)
|
|
66
|
+
case value
|
|
67
|
+
when Hash
|
|
68
|
+
unless value.keys.all? { |key| key.is_a?(String) || key.is_a?(Symbol) } && value.keys.map(&:to_s).uniq.size == value.size
|
|
69
|
+
raise InputError, "ambiguous receipt keys"
|
|
70
|
+
end
|
|
71
|
+
value.each_value { |item| validate_serialized_keys!(item) }
|
|
72
|
+
when Array then value.each { |item| validate_serialized_keys!(item) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
private_class_method :validate_serialized_keys!
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Deployment
|
|
5
|
+
# 根据收据建议宿主的下一步动作;不调度,也不执行动作。
|
|
6
|
+
# 未知结果包括已经开始、应用或接受但尚未确认效果的步骤。
|
|
7
|
+
class Recovery
|
|
8
|
+
def decision(receipt:)
|
|
9
|
+
raise InputError, "receipt required" unless receipt.is_a?(Receipt)
|
|
10
|
+
return :no_changes if receipt.no_changes?
|
|
11
|
+
return :verify if receipt.unknown?
|
|
12
|
+
return :review if receipt.failed?
|
|
13
|
+
return :complete if receipt.verified?
|
|
14
|
+
|
|
15
|
+
:resume
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Deployment
|
|
5
|
+
# 步骤身份、动作参数和预期状态;具体动作的业务校验由领域适配器负责。
|
|
6
|
+
# 身份参与幂等键计算;此对象本身不代表已经提交或已经执行。
|
|
7
|
+
class Step < Data.define(:id, :action, :parameters, :expected)
|
|
8
|
+
def initialize(id:, action:, parameters: {}, expected: {})
|
|
9
|
+
raise InputError, "step parameters and expected state must be hashes" unless parameters.is_a?(Hash) && expected.is_a?(Hash)
|
|
10
|
+
|
|
11
|
+
super(id: Values.text(id, name: "step id"), action: Values.text(action, name: "step action"),
|
|
12
|
+
parameters: Values.copy(parameters), expected: Values.copy(expected))
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
# A collection result for one explicitly requested scope; empty data is not a success signal.
|
|
5
|
+
class Dimension < Data.define(:name, :status, :records, :started_at, :completed_at, :evidence, :diagnostics)
|
|
6
|
+
STATUSES = %i[complete partial unsupported failed not_requested].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(name:, status:, records: [], started_at: nil, completed_at: nil, evidence: [], diagnostics: [])
|
|
9
|
+
name = Values.text(name.is_a?(Symbol) ? name.to_s : name, name: "dimension name").to_sym
|
|
10
|
+
status = status.to_sym if status.is_a?(String)
|
|
11
|
+
raise InputError, "invalid dimension status" unless STATUSES.include?(status)
|
|
12
|
+
raise InputError, "dimension records must be an Array" unless records.is_a?(Array)
|
|
13
|
+
if %i[unsupported failed not_requested].include?(status) && !records.empty?
|
|
14
|
+
raise InputError, "unavailable dimensions cannot contain records"
|
|
15
|
+
end
|
|
16
|
+
started_at = Values.time(started_at, name: "collection start", optional: true)
|
|
17
|
+
completed_at = Values.time(completed_at, name: "collection end", optional: true)
|
|
18
|
+
if (!started_at.nil? ^ !completed_at.nil?) || (started_at && completed_at < started_at)
|
|
19
|
+
raise InputError, "invalid collection time window"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
super(name: name, status: status, records: Values.copy(records), started_at: started_at, completed_at: completed_at,
|
|
23
|
+
evidence: Values.copy(evidence), diagnostics: Values.copy(diagnostics))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def complete? = status == :complete
|
|
27
|
+
def partial? = status == :partial
|
|
28
|
+
def unavailable? = %i[unsupported failed not_requested].include?(status)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
class Error < StandardError
|
|
5
|
+
attr_reader :code, :stage, :details
|
|
6
|
+
|
|
7
|
+
def initialize(message = "network operation failed", code: :operation_failed, stage: nil, details: {})
|
|
8
|
+
@code = code.to_sym
|
|
9
|
+
@stage = stage&.to_sym
|
|
10
|
+
@details = Values.copy(details)
|
|
11
|
+
super(message)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Integrations
|
|
5
|
+
module Bluecat
|
|
6
|
+
# BlueCat REST v1 协议边界;分页必须正常结束后才能向调用者
|
|
7
|
+
# 返回集合;耗尽页数预算不会将部分数据作为成功结果返回。
|
|
8
|
+
class Client
|
|
9
|
+
BASE_PATH = "Services/REST/v1"
|
|
10
|
+
|
|
11
|
+
def initialize(base_url: nil, username:, password:, http: nil, **http_options)
|
|
12
|
+
@username = Values.text(username, name: "BlueCat username")
|
|
13
|
+
raise InputError, "BlueCat password must be nonempty text" unless password.is_a?(String) && password.present?
|
|
14
|
+
|
|
15
|
+
# 密码是凭据字节,不能像普通名称一样去除前后空格。
|
|
16
|
+
@password = password.dup.freeze
|
|
17
|
+
@http = http || Http::Client.new(base_url: base_url, **http_options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def inspect = "#<#{self.class.name}>"
|
|
21
|
+
|
|
22
|
+
def json(endpoint, **query)
|
|
23
|
+
request(:get, endpoint, query).json
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def entity(endpoint, **query)
|
|
27
|
+
validate_entity(json(endpoint, **query), allow_missing: true)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def entities(parent_id:, type:, page_size: 100, max_pages: 100)
|
|
31
|
+
page_size = Values.integer(page_size, name: "page_size", min: 1, max: 1000)
|
|
32
|
+
max_pages = Values.integer(max_pages, name: "max_pages", min: 1)
|
|
33
|
+
all = []
|
|
34
|
+
max_pages.times do |page|
|
|
35
|
+
rows = json("getEntities", parentId: parent_id, type: type, start: page * page_size, count: page_size)
|
|
36
|
+
invalid! unless rows.is_a?(Array) && rows.size <= page_size
|
|
37
|
+
rows = rows.map { |row| validate_entity(row) }
|
|
38
|
+
invalid! unless (all + rows).map { |row| row["id"] }.uniq.size == all.size + rows.size
|
|
39
|
+
all.concat(rows)
|
|
40
|
+
return Values.copy(all) if rows.size < page_size
|
|
41
|
+
end
|
|
42
|
+
raise Netstack::Error.new("BlueCat entity list is incomplete", code: :incomplete_snapshot)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write(method, endpoint, query:, returns_id: false)
|
|
46
|
+
response = request(method, endpoint, query)
|
|
47
|
+
if returns_id
|
|
48
|
+
id = response.json
|
|
49
|
+
invalid! unless id.is_a?(Integer) && id.positive?
|
|
50
|
+
id
|
|
51
|
+
else
|
|
52
|
+
invalid! unless ["", "null", "true"].include?(response.body.strip)
|
|
53
|
+
true
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def validate_entity(value, allow_missing: false)
|
|
58
|
+
invalid! unless value.is_a?(Hash) && value["id"].is_a?(Integer) && value["id"] >= 0
|
|
59
|
+
return nil if allow_missing && value["id"].zero?
|
|
60
|
+
invalid! unless value["id"].positive? && value["name"].is_a?(String) && value["type"].is_a?(String) && value["properties"].is_a?(String)
|
|
61
|
+
Values.copy(value)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def request(method, endpoint, query)
|
|
67
|
+
raise InputError, "Invalid BlueCat endpoint" unless endpoint.is_a?(String) && endpoint.match?(/\A[a-zA-Z][a-zA-Z0-9]+\z/)
|
|
68
|
+
authenticate! unless @token
|
|
69
|
+
@http.request(method, "#{BASE_PATH}/#{endpoint}", query: query, headers: { "Authorization" => "BAMAuthToken: #{@token}" })
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def authenticate!
|
|
73
|
+
response = @http.request(:get, "#{BASE_PATH}/login", query: { username: @username, password: @password })
|
|
74
|
+
@token = response.body[/BAMAuthToken:\s+([^\s"<>]+)/, 1]
|
|
75
|
+
raise Netstack::Error.new("BlueCat authentication failed", code: :authentication_failed) unless @token
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def invalid!
|
|
79
|
+
raise Netstack::Error.new("Invalid BlueCat response", code: :invalid_response, stage: :response)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require "timeout"
|
|
8
|
+
|
|
9
|
+
module Netstack
|
|
10
|
+
# 显式调用的外部协议客户端;构造和加载常量不会建立连接。
|
|
11
|
+
module Integrations
|
|
12
|
+
module Http
|
|
13
|
+
# One request, no redirects or implicit retries. Transport receives keyword
|
|
14
|
+
# arguments and returns Response, which also makes protocol tests offline.
|
|
15
|
+
class Client
|
|
16
|
+
METHODS = %w[get post put patch delete head].freeze
|
|
17
|
+
FORBIDDEN_HEADERS = %w[host content-length connection transfer-encoding proxy-authorization].freeze
|
|
18
|
+
|
|
19
|
+
def initialize(base_url:, headers: {}, open_timeout: 5, read_timeout: 30,
|
|
20
|
+
write_timeout: 30, request_timeout: 60, max_response_bytes: 2_097_152, verify_tls: true, transport: nil)
|
|
21
|
+
@base_uri = parse_uri(base_url)
|
|
22
|
+
@headers = validate_headers(headers)
|
|
23
|
+
@timeouts = { open_timeout: open_timeout, read_timeout: read_timeout, write_timeout: write_timeout }
|
|
24
|
+
invalid!("HTTP timeouts must be positive numbers") unless @timeouts.values.all? { |value| value.is_a?(Numeric) && value.finite? && value.positive? }
|
|
25
|
+
invalid!("HTTP request timeout must be positive") unless request_timeout.is_a?(Numeric) && request_timeout.finite? && request_timeout.positive?
|
|
26
|
+
@request_timeout = request_timeout
|
|
27
|
+
invalid!("Response byte limit must be a positive integer") unless max_response_bytes.is_a?(Integer) && max_response_bytes.positive?
|
|
28
|
+
invalid!("verify_tls must be boolean") unless [true, false].include?(verify_tls)
|
|
29
|
+
@max_response_bytes = max_response_bytes
|
|
30
|
+
@verify_tls = verify_tls
|
|
31
|
+
@transport = transport
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inspect = "#<#{self.class.name}>"
|
|
35
|
+
|
|
36
|
+
def request(method, path = "", query: {}, headers: {}, json: nil, body: nil)
|
|
37
|
+
method = method.to_s.downcase
|
|
38
|
+
invalid!("Unsupported HTTP method") unless METHODS.include?(method)
|
|
39
|
+
invalid!("Supply json or body, not both") if !json.nil? && !body.nil?
|
|
40
|
+
uri = request_uri(path, query)
|
|
41
|
+
request_headers = @headers.merge(validate_headers(headers))
|
|
42
|
+
unless json.nil?
|
|
43
|
+
request_headers["Content-Type"] ||= "application/json"
|
|
44
|
+
body = JSON.generate(json, allow_nan: false)
|
|
45
|
+
end
|
|
46
|
+
invalid!("HTTP body must be text") unless body.nil? || body.is_a?(String)
|
|
47
|
+
options = { method: method, url: uri.to_s, headers: request_headers, body: body,
|
|
48
|
+
**@timeouts, max_response_bytes: @max_response_bytes, verify_tls: @verify_tls }
|
|
49
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @request_timeout
|
|
50
|
+
response = Timeout.timeout(@request_timeout) do
|
|
51
|
+
@transport ? @transport.call(**options) : transmit(**options, deadline: deadline)
|
|
52
|
+
end
|
|
53
|
+
raise Timeout::Error if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
54
|
+
raise Error.new("Invalid HTTP transport response", code: "invalid_response", stage: "response") unless response.is_a?(Response)
|
|
55
|
+
|
|
56
|
+
check_size!(response.body.bytesize)
|
|
57
|
+
unless (200..299).cover?(response.status)
|
|
58
|
+
raise Error.new("HTTP request was not successful", code: "http_status", stage: "response",
|
|
59
|
+
details: { "status" => response.status })
|
|
60
|
+
end
|
|
61
|
+
response
|
|
62
|
+
rescue Timeout::Error
|
|
63
|
+
raise Error.new("HTTP request timed out", code: "http_timeout", stage: "transport"), cause: nil
|
|
64
|
+
rescue SocketError, IOError, SystemCallError, OpenSSL::SSL::SSLError, Net::HTTPBadResponse, Net::ProtocolError
|
|
65
|
+
raise Error.new("HTTP connection failed", code: "http_connection_failed", stage: "transport"), cause: nil
|
|
66
|
+
rescue JSON::GeneratorError
|
|
67
|
+
invalid!("Request is not valid JSON")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def transmit(method:, url:, headers:, body:, open_timeout:, read_timeout:, write_timeout:, max_response_bytes:, verify_tls:, deadline:)
|
|
73
|
+
uri = URI.parse(url)
|
|
74
|
+
http = Net::HTTP.new(uri.host, uri.port, nil)
|
|
75
|
+
http.use_ssl = uri.scheme == "https"
|
|
76
|
+
http.verify_mode = verify_tls ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
|
|
77
|
+
http.open_timeout = open_timeout
|
|
78
|
+
http.read_timeout = read_timeout
|
|
79
|
+
http.write_timeout = write_timeout
|
|
80
|
+
http.max_retries = 0
|
|
81
|
+
request = Net::HTTPGenericRequest.new(method.upcase, !body.nil?, method != "head", uri.request_uri, headers)
|
|
82
|
+
request.body = body if body
|
|
83
|
+
http.start do
|
|
84
|
+
http.request(request) do |response|
|
|
85
|
+
bytes = +""
|
|
86
|
+
response.read_body do |chunk|
|
|
87
|
+
raise Timeout::Error if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
88
|
+
check_size!(bytes.bytesize + chunk.bytesize, max_response_bytes)
|
|
89
|
+
bytes << chunk
|
|
90
|
+
end
|
|
91
|
+
return Response.new(status: response.code.to_i, headers: response.to_hash.transform_values { |values| values.join(", ") }, body: bytes)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def parse_uri(value)
|
|
97
|
+
invalid!("HTTP base URL must be text") unless value.is_a?(String)
|
|
98
|
+
uri = URI.parse(value)
|
|
99
|
+
unless %w[http https].include?(uri.scheme) && uri.host && !uri.userinfo && !uri.fragment
|
|
100
|
+
invalid!("HTTP base URL must identify an HTTP origin without credentials or fragment")
|
|
101
|
+
end
|
|
102
|
+
uri
|
|
103
|
+
rescue URI::InvalidURIError
|
|
104
|
+
invalid!("Invalid HTTP URL")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def request_uri(path, query)
|
|
108
|
+
invalid!("HTTP path must be text") unless path.is_a?(String)
|
|
109
|
+
invalid!("HTTP query must be a hash") unless query.is_a?(Hash)
|
|
110
|
+
uri = path.empty? ? @base_uri.dup : URI.join(@base_uri.to_s.sub(%r{/\z}, "") + "/", path)
|
|
111
|
+
unless [uri.scheme, uri.host, uri.port] == [@base_uri.scheme, @base_uri.host, @base_uri.port] && !uri.userinfo && !uri.fragment
|
|
112
|
+
invalid!("Request path must stay within the configured origin")
|
|
113
|
+
end
|
|
114
|
+
unless query.empty?
|
|
115
|
+
encoded = URI.encode_www_form(query)
|
|
116
|
+
uri.query = [uri.query, encoded].compact.reject(&:empty?).join("&")
|
|
117
|
+
end
|
|
118
|
+
uri
|
|
119
|
+
rescue URI::InvalidURIError
|
|
120
|
+
invalid!("Invalid HTTP path")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def validate_headers(headers)
|
|
124
|
+
invalid!("HTTP headers must be a hash") unless headers.is_a?(Hash)
|
|
125
|
+
headers.to_h do |key, value|
|
|
126
|
+
name = key.to_s
|
|
127
|
+
unless name.match?(/\A[!#$%&'*+.^_`|~0-9a-z-]+\z/i) && value.is_a?(String) && !value.match?(/[\r\n\x00]/) && !FORBIDDEN_HEADERS.include?(name.downcase)
|
|
128
|
+
invalid!("Invalid HTTP header")
|
|
129
|
+
end
|
|
130
|
+
[name.dup.freeze, value.dup.freeze]
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def check_size!(bytes, maximum = @max_response_bytes)
|
|
135
|
+
return if bytes <= maximum
|
|
136
|
+
|
|
137
|
+
raise Error.new("HTTP response exceeds the byte limit", code: "response_too_large", stage: "response")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def invalid!(message)
|
|
141
|
+
raise Netstack::InputError.new(message, code: "invalid_http_input", stage: "input")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Netstack
|
|
6
|
+
module Integrations
|
|
7
|
+
module Http
|
|
8
|
+
class Response
|
|
9
|
+
attr_reader :status, :headers, :body
|
|
10
|
+
|
|
11
|
+
def initialize(status:, headers: {}, body: "")
|
|
12
|
+
@status = Integer(status)
|
|
13
|
+
@headers = headers.to_h.transform_keys { |key| key.to_s.downcase }.transform_values { |value| value.to_s.dup.freeze }.freeze
|
|
14
|
+
@body = String(body).dup.freeze
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def json
|
|
19
|
+
JSON.parse(body)
|
|
20
|
+
rescue JSON::ParserError, EncodingError
|
|
21
|
+
raise Error.new("HTTP response is not valid JSON", code: "invalid_json", stage: "response"), cause: nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def inspect = "#<#{self.class.name} status=#{status} bytes=#{body.bytesize}>"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Netstack
|
|
4
|
+
module Integrations
|
|
5
|
+
module HttpDelivery
|
|
6
|
+
# 将工件引用接入 Deployment 的提交与回读协议。
|
|
7
|
+
# 每次新提交都重新解析工件并核对摘要;恢复按操作身份查询。
|
|
8
|
+
class Adapter
|
|
9
|
+
def initialize(client:, artifact_resolver:)
|
|
10
|
+
@client = client
|
|
11
|
+
@artifacts = Configuration::ArtifactResolver.new(resolver: artifact_resolver)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def preflight(plan:, previous:, credentials:) # rubocop:disable Naming/PredicateMethod -- Deployment adapter protocol.
|
|
15
|
+
@client.validate_credentials!(credentials)
|
|
16
|
+
previous_ids = previous ? previous.steps.map { |step| step.fetch(:step_id) } : []
|
|
17
|
+
plan.steps.each { |step| @artifacts.resolve(step).validate_target!(plan.target) unless previous_ids.include?(step.id) }
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def apply(step:, target:, credentials:, idempotency_key:)
|
|
22
|
+
artifact = @artifacts.resolve(step)
|
|
23
|
+
result = @client.submit(target: target, artifact: artifact, credentials: credentials, idempotency_key: idempotency_key,
|
|
24
|
+
request_no: step.parameters[:request_no], target_id: step.parameters[:target_id])
|
|
25
|
+
response(result, submitting: true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def verify(step:, target:, credentials:, receipt:) # rubocop:disable Lint/UnusedMethodArgument -- Deployment adapter protocol; lookup needs no device credentials.
|
|
29
|
+
# Even a previous POST terminal reply is reconciled using the same identity.
|
|
30
|
+
result = @client.status(remote_id: receipt[:remote_id], idempotency_key: receipt.fetch(:idempotency_key))
|
|
31
|
+
response(result, submitting: false)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def response(result, submitting:)
|
|
37
|
+
status = case result.status
|
|
38
|
+
when :succeeded then submitting ? :applied : :verified
|
|
39
|
+
when :running then submitting ? :accepted : :unknown
|
|
40
|
+
when :failed then :failed
|
|
41
|
+
else :unknown
|
|
42
|
+
end
|
|
43
|
+
value = { status: status, evidence: { remote_status: result.status } }
|
|
44
|
+
value[:remote_id] = result.remote_id if result.remote_id
|
|
45
|
+
value[:error_code] = :remote_rejected if result.failed?
|
|
46
|
+
value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|