empeira 0.1.0.pre.alpha.1
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/LICENSE +661 -0
- data/README.md +150 -0
- data/bin/empeira +18 -0
- data/config/defaults.yaml +229 -0
- data/docs/addons.md +331 -0
- data/docs/architecture.md +277 -0
- data/docs/configuration.md +1204 -0
- data/docs/development.md +472 -0
- data/docs/hiera-and-eyaml.md +361 -0
- data/docs/installation.md +280 -0
- data/docs/networking.md +362 -0
- data/docs/nodes.md +642 -0
- data/docs/proxy.md +136 -0
- data/docs/troubleshooting.md +361 -0
- data/lib/empeira/addons/environment.rb +82 -0
- data/lib/empeira/addons/error.rb +14 -0
- data/lib/empeira/addons/installation.rb +119 -0
- data/lib/empeira/addons/manifest.rb +112 -0
- data/lib/empeira/addons/service.rb +50 -0
- data/lib/empeira/addons/storage.rb +94 -0
- data/lib/empeira/agent/acquisition.rb +120 -0
- data/lib/empeira/agent/artifact.rb +41 -0
- data/lib/empeira/agent/authentication.rb +122 -0
- data/lib/empeira/agent/cache.rb +122 -0
- data/lib/empeira/agent/cache_storage.rb +82 -0
- data/lib/empeira/agent/download.rb +84 -0
- data/lib/empeira/agent/native_commands.rb +84 -0
- data/lib/empeira/agent/release_sources.rb +64 -0
- data/lib/empeira/agent/target.rb +66 -0
- data/lib/empeira/agent/versions.rb +29 -0
- data/lib/empeira/application.rb +86 -0
- data/lib/empeira/build.json +5 -0
- data/lib/empeira/build_info.rb +46 -0
- data/lib/empeira/cli/addons.rb +45 -0
- data/lib/empeira/cli/base.rb +73 -0
- data/lib/empeira/cli/completion.rb +42 -0
- data/lib/empeira/cli/config.rb +22 -0
- data/lib/empeira/cli/main.rb +114 -0
- data/lib/empeira/cli/node.rb +87 -0
- data/lib/empeira/cli/progress.rb +169 -0
- data/lib/empeira/cli/proxy.rb +36 -0
- data/lib/empeira/cli/update.rb +23 -0
- data/lib/empeira/completion/addons.rb +93 -0
- data/lib/empeira/completion/bash.rb +62 -0
- data/lib/empeira/completion/catalog.rb +170 -0
- data/lib/empeira/configuration/additional_services.rb +74 -0
- data/lib/empeira/configuration/addons.rb +85 -0
- data/lib/empeira/configuration/agent_schema.rb +163 -0
- data/lib/empeira/configuration/agent_values.rb +54 -0
- data/lib/empeira/configuration/bootstrap_guests.rb +42 -0
- data/lib/empeira/configuration/command_mocks.rb +65 -0
- data/lib/empeira/configuration/display.rb +20 -0
- data/lib/empeira/configuration/dns_rewrites.rb +58 -0
- data/lib/empeira/configuration/document.rb +67 -0
- data/lib/empeira/configuration/eyaml.rb +46 -0
- data/lib/empeira/configuration/hiera_modulepath.rb +40 -0
- data/lib/empeira/configuration/hiera_mount_schema.rb +62 -0
- data/lib/empeira/configuration/hiera_mounts.rb +83 -0
- data/lib/empeira/configuration/image_schema.rb +68 -0
- data/lib/empeira/configuration/loader.rb +70 -0
- data/lib/empeira/configuration/merge.rb +48 -0
- data/lib/empeira/configuration/network_redirects.rb +73 -0
- data/lib/empeira/configuration/node_images.rb +64 -0
- data/lib/empeira/configuration/schema.rb +215 -0
- data/lib/empeira/configuration/server_mounts.rb +106 -0
- data/lib/empeira/configuration/service_configuration.rb +40 -0
- data/lib/empeira/configuration/ssh_preferences.rb +83 -0
- data/lib/empeira/configuration/user.rb +46 -0
- data/lib/empeira/configuration/vm_interfaces.rb +147 -0
- data/lib/empeira/control_plane/additional_configurations.rb +106 -0
- data/lib/empeira/control_plane/browser.rb +73 -0
- data/lib/empeira/control_plane/controller.rb +763 -0
- data/lib/empeira/control_plane/discovery.rb +69 -0
- data/lib/empeira/control_plane/environment.rb +101 -0
- data/lib/empeira/control_plane/files.rb +263 -0
- data/lib/empeira/control_plane/health.rb +104 -0
- data/lib/empeira/control_plane/plan.rb +356 -0
- data/lib/empeira/control_plane/storage.rb +48 -0
- data/lib/empeira/errors.rb +9 -0
- data/lib/empeira/execution/console.rb +45 -0
- data/lib/empeira/execution/diagnostics.rb +210 -0
- data/lib/empeira/execution/launch_error.rb +14 -0
- data/lib/empeira/execution/result.rb +11 -0
- data/lib/empeira/execution/runner.rb +235 -0
- data/lib/empeira/execution/terminal.rb +35 -0
- data/lib/empeira/hostname_pattern.rb +14 -0
- data/lib/empeira/images/configuration.rb +44 -0
- data/lib/empeira/images/identity.rb +30 -0
- data/lib/empeira/images/recipe.rb +22 -0
- data/lib/empeira/images/reference.rb +43 -0
- data/lib/empeira/images/source.rb +19 -0
- data/lib/empeira/immutable.rb +19 -0
- data/lib/empeira/infrastructure/cleanup.rb +328 -0
- data/lib/empeira/infrastructure/definition.rb +59 -0
- data/lib/empeira/infrastructure/owned_files.rb +63 -0
- data/lib/empeira/infrastructure/recovery_inventory.rb +53 -0
- data/lib/empeira/infrastructure/service.rb +237 -0
- data/lib/empeira/infrastructure/status.rb +73 -0
- data/lib/empeira/infrastructure/store.rb +274 -0
- data/lib/empeira/modules/events.rb +67 -0
- data/lib/empeira/modules/git_safety.rb +60 -0
- data/lib/empeira/modules/host_git.rb +91 -0
- data/lib/empeira/modules/image.rb +27 -0
- data/lib/empeira/modules/installer.rb +67 -0
- data/lib/empeira/modules/request.rb +68 -0
- data/lib/empeira/modules/state.rb +87 -0
- data/lib/empeira/modules/storage.rb +35 -0
- data/lib/empeira/network/bootstrap_proxy.rb +157 -0
- data/lib/empeira/network/definition.rb +61 -0
- data/lib/empeira/network/direct_egress.rb +133 -0
- data/lib/empeira/network/egress.rb +27 -0
- data/lib/empeira/network/gateway.rb +79 -0
- data/lib/empeira/network/host_policy.rb +53 -0
- data/lib/empeira/network/naming.rb +28 -0
- data/lib/empeira/network/peer/adapter_image.rb +32 -0
- data/lib/empeira/network/peer/allocation.rb +45 -0
- data/lib/empeira/network/peer/attachment.rb +27 -0
- data/lib/empeira/network/peer/backend.rb +108 -0
- data/lib/empeira/network/peer/channel.rb +133 -0
- data/lib/empeira/network/peer/channel_server.rb +71 -0
- data/lib/empeira/network/peer/docker_adapter.rb +165 -0
- data/lib/empeira/network/peer/helper_security.rb +40 -0
- data/lib/empeira/network/peer/layout.rb +55 -0
- data/lib/empeira/network/peer/linux_podman.rb +114 -0
- data/lib/empeira/network/peer/podman_machine.rb +112 -0
- data/lib/empeira/network/policy.rb +39 -0
- data/lib/empeira/network/proxy_bindings.rb +52 -0
- data/lib/empeira/network/proxy_policy.rb +64 -0
- data/lib/empeira/network/redirects.rb +89 -0
- data/lib/empeira/network/resource.rb +50 -0
- data/lib/empeira/node/agent_installer.rb +31 -0
- data/lib/empeira/node/agent_package.rb +125 -0
- data/lib/empeira/node/agent_repository.rb +160 -0
- data/lib/empeira/node/apt_configuration.rb +85 -0
- data/lib/empeira/node/bootstrap.rb +17 -0
- data/lib/empeira/node/certificates.rb +98 -0
- data/lib/empeira/node/command_mock.rb +38 -0
- data/lib/empeira/node/command_mocks.rb +114 -0
- data/lib/empeira/node/container.rb +134 -0
- data/lib/empeira/node/container_agent_bootstrap.rb +43 -0
- data/lib/empeira/node/container_bootstrap.rb +52 -0
- data/lib/empeira/node/container_guest.rb +42 -0
- data/lib/empeira/node/container_lifecycle.rb +145 -0
- data/lib/empeira/node/container_preparation.rb +144 -0
- data/lib/empeira/node/container_state.rb +129 -0
- data/lib/empeira/node/definition.rb +20 -0
- data/lib/empeira/node/diagnostic_command.rb +19 -0
- data/lib/empeira/node/dnf_agent_repository.rb +202 -0
- data/lib/empeira/node/external_fact.rb +21 -0
- data/lib/empeira/node/image.rb +38 -0
- data/lib/empeira/node/interactive_tools.rb +33 -0
- data/lib/empeira/node/interface.rb +18 -0
- data/lib/empeira/node/inventory.rb +143 -0
- data/lib/empeira/node/package_bootstrap.rb +129 -0
- data/lib/empeira/node/package_proxy.rb +102 -0
- data/lib/empeira/node/package_sources.rb +19 -0
- data/lib/empeira/node/puppet_command.rb +12 -0
- data/lib/empeira/node/rpm_configuration.rb +62 -0
- data/lib/empeira/node/run_request.rb +52 -0
- data/lib/empeira/node/runtime_proxy.rb +86 -0
- data/lib/empeira/node/service.rb +72 -0
- data/lib/empeira/node/ssh_client.rb +25 -0
- data/lib/empeira/node/ssh_credentials.rb +78 -0
- data/lib/empeira/node/ssh_endpoint.rb +58 -0
- data/lib/empeira/node/ssh_identity.rb +37 -0
- data/lib/empeira/node/user_ssh.rb +60 -0
- data/lib/empeira/node/vm.rb +89 -0
- data/lib/empeira/node/vm_bootstrap.rb +53 -0
- data/lib/empeira/node/vm_lifecycle.rb +212 -0
- data/lib/empeira/node/vm_preflight.rb +37 -0
- data/lib/empeira/node/vm_preparation.rb +161 -0
- data/lib/empeira/node/vm_state.rb +161 -0
- data/lib/empeira/platform/facts.rb +66 -0
- data/lib/empeira/platform/kvm.rb +91 -0
- data/lib/empeira/platform/locations.rb +51 -0
- data/lib/empeira/platform/resolvers.rb +85 -0
- data/lib/empeira/platform/routes.rb +50 -0
- data/lib/empeira/progress.rb +70 -0
- data/lib/empeira/project_root.rb +47 -0
- data/lib/empeira/providers/lifecycle.rb +96 -0
- data/lib/empeira/providers/registry.rb +21 -0
- data/lib/empeira/providers/resource.rb +32 -0
- data/lib/empeira/providers.rb +55 -0
- data/lib/empeira/runtime/agent_helper.rb +34 -0
- data/lib/empeira/runtime/container.rb +184 -0
- data/lib/empeira/runtime/diagnostic_execution.rb +52 -0
- data/lib/empeira/runtime/docker.rb +87 -0
- data/lib/empeira/runtime/image_metadata.rb +83 -0
- data/lib/empeira/runtime/interface.rb +8 -0
- data/lib/empeira/runtime/mount_probe.rb +57 -0
- data/lib/empeira/runtime/podman.rb +122 -0
- data/lib/empeira/runtime/recipe_build.rb +103 -0
- data/lib/empeira/runtime/registry_diagnostics.rb +23 -0
- data/lib/empeira/runtime/service_arguments.rb +55 -0
- data/lib/empeira/runtime/service_images.rb +118 -0
- data/lib/empeira/runtime/service_inspection.rb +159 -0
- data/lib/empeira/runtime/services.rb +120 -0
- data/lib/empeira/runtime/update_helper.rb +97 -0
- data/lib/empeira/runtime/workspace_network.rb +69 -0
- data/lib/empeira/server/environment_configuration.rb +53 -0
- data/lib/empeira/server/http.rb +38 -0
- data/lib/empeira/server/puppetdb_runtime.rb +74 -0
- data/lib/empeira/server/relay_certificate.rb +76 -0
- data/lib/empeira/server/runtime.rb +90 -0
- data/lib/empeira/services/definition.rb +42 -0
- data/lib/empeira/updates/images.rb +104 -0
- data/lib/empeira/updates/modules.rb +45 -0
- data/lib/empeira/updates/service.rb +45 -0
- data/lib/empeira/version_requirement.rb +23 -0
- data/lib/empeira/vm/agent.rb +66 -0
- data/lib/empeira/vm/bootstrap_requirements.rb +69 -0
- data/lib/empeira/vm/certificate_runtime.rb +30 -0
- data/lib/empeira/vm/cloud_init.rb +171 -0
- data/lib/empeira/vm/disk.rb +136 -0
- data/lib/empeira/vm/guest.rb +123 -0
- data/lib/empeira/vm/guest_channel.rb +110 -0
- data/lib/empeira/vm/guest_network.rb +94 -0
- data/lib/empeira/vm/guest_output.rb +52 -0
- data/lib/empeira/vm/guest_shell.rb +79 -0
- data/lib/empeira/vm/guest_upload.rb +43 -0
- data/lib/empeira/vm/image_cache.rb +77 -0
- data/lib/empeira/vm/image_source.rb +91 -0
- data/lib/empeira/vm/interface.rb +8 -0
- data/lib/empeira/vm/interface_ownership.rb +124 -0
- data/lib/empeira/vm/interface_routes.rb +101 -0
- data/lib/empeira/vm/interfaces.rb +240 -0
- data/lib/empeira/vm/management.rb +73 -0
- data/lib/empeira/vm/prerequisites.rb +42 -0
- data/lib/empeira/vm/qemu.rb +83 -0
- data/lib/empeira/vm/qemu_runtime.rb +404 -0
- data/lib/empeira/vm/removal.rb +30 -0
- data/lib/empeira/vm/root_disk.rb +92 -0
- data/lib/empeira/vm/session_lock.rb +56 -0
- data/lib/empeira/vm/system_ssh.rb +29 -0
- data/lib/empeira/workspace.rb +42 -0
- data/lib/empeira.rb +209 -0
- data/resources/browser/relay.rb +22 -0
- data/resources/gateway/Containerfile +8 -0
- data/resources/gateway/bridge.rb +66 -0
- data/resources/gateway/configure.rb +323 -0
- data/resources/modules/Containerfile +15 -0
- data/resources/modules/Gemfile +5 -0
- data/resources/modules/Gemfile.lock +105 -0
- data/resources/modules/worker.rb +161 -0
- data/resources/network/adapter/Containerfile +13 -0
- data/resources/network/adapter/bridge_linux.go +51 -0
- data/resources/network/adapter/framing.go +45 -0
- data/resources/network/adapter/framing_test.go +53 -0
- data/resources/network/adapter/go.mod +3 -0
- data/resources/network/adapter/main_linux.go +273 -0
- data/resources/network/channel.rb +18 -0
- data/resources/network/ssh_connect.rb +32 -0
- data/resources/nodes/apt_sources.rb +66 -0
- data/resources/nodes/debian/Containerfile +10 -0
- data/resources/nodes/diagnostic_exec.rb +109 -0
- data/resources/nodes/interactive_tools.rb +139 -0
- data/resources/nodes/managed_file.rb +91 -0
- data/resources/nodes/management/guest_agent.py +415 -0
- data/resources/nodes/management/setup.sh +20 -0
- data/resources/nodes/management/shell_rc.sh +12 -0
- data/resources/nodes/management/system_account.sh +24 -0
- data/resources/nodes/management/system_home.sh +15 -0
- data/resources/nodes/rpm/Containerfile +10 -0
- data/resources/nodes/runtime_proxy.rb +244 -0
- data/resources/nodes/ssh/proxy.rb +16 -0
- data/resources/nodes/ssh/sshd_config +15 -0
- data/resources/nodes/ssh/start +6 -0
- data/resources/proxy/Containerfile +10 -0
- data/resources/relay/Containerfile +5 -0
- data/resources/relay/puppetdb.rb +81 -0
- data/resources/relay/server_environment.rb +10 -0
- data/resources/relay/server_start.sh +41 -0
- metadata +326 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module CLI
|
|
7
|
+
class Addons < Base
|
|
8
|
+
desc 'list', 'List configured addons and local installation status'
|
|
9
|
+
def list
|
|
10
|
+
service = application.addons
|
|
11
|
+
service.names.each do |name|
|
|
12
|
+
say "#{name}: #{local_status(service.installation(name))}"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
desc 'info NAME', 'Show source, reference, commit, path and manifest'
|
|
17
|
+
def info(name)
|
|
18
|
+
entry = application.addons.installation(name)
|
|
19
|
+
say "Addon: #{name}\nGit: #{entry.source}\nRef: #{entry.ref}"
|
|
20
|
+
local = entry.local
|
|
21
|
+
return say('Status: missing; run empeira addons sync') unless local
|
|
22
|
+
|
|
23
|
+
commit, path, manifest = local
|
|
24
|
+
say "Commit: #{commit}\nPath: #{path}\n#{YAML.dump(manifest.data)}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
%w[sync update].each do |operation|
|
|
28
|
+
desc operation, 'Synchronize configured Git addons without starting infrastructure'
|
|
29
|
+
define_method(operation) do
|
|
30
|
+
application.addons.synchronize
|
|
31
|
+
say 'Configured addons synchronized.'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
no_commands do
|
|
36
|
+
def local_status(entry)
|
|
37
|
+
local = entry.local
|
|
38
|
+
local ? "installed (#{entry.ref}, #{local.first})" : "missing (#{entry.ref})"
|
|
39
|
+
rescue Empeira::Addons::Error => e
|
|
40
|
+
"invalid (#{e.message})"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require_relative 'progress'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module CLI
|
|
8
|
+
class Base < Thor
|
|
9
|
+
check_unknown_options!
|
|
10
|
+
class_option :container_engine, type: :string, enum: Configuration::Schema::CONTAINER_ENGINES,
|
|
11
|
+
desc: 'Override runtime.container_engine'
|
|
12
|
+
|
|
13
|
+
def self.exit_on_failure?
|
|
14
|
+
true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Extract only the explicit guest argv before Thor reparses a nested subcommand.
|
|
18
|
+
def self.start(given_args = ARGV, config = {})
|
|
19
|
+
boundary = given_args.index('--')
|
|
20
|
+
prefix = boundary && given_args.take(boundary)
|
|
21
|
+
if prefix && (prefix.each_cons(2).include?(%w[node exec]) ||
|
|
22
|
+
(name == 'Empeira::CLI::Node' && prefix.first == 'exec'))
|
|
23
|
+
config = config.merge(guest_arguments: given_args.drop(boundary + 1))
|
|
24
|
+
given_args = prefix
|
|
25
|
+
end
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(args = [], local_options = {}, config = {})
|
|
30
|
+
@guest_arguments = config[:guest_arguments]
|
|
31
|
+
@addon_arguments = config[:addon_arguments]
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Thor groups presentation helpers outside its command DSL.
|
|
36
|
+
# rubocop:disable-next Metrics/BlockLength
|
|
37
|
+
no_commands do
|
|
38
|
+
def application(node: false, progress: Empeira::Progress.new, recovery: false)
|
|
39
|
+
overrides = runtime_overrides
|
|
40
|
+
overrides = Configuration::Merge.call(overrides, node_overrides) if node
|
|
41
|
+
Application.new(project_path: Dir.pwd, overrides: overrides, progress: progress, recovery: recovery,
|
|
42
|
+
recovery_path: recovery ? options[:project_path] : nil)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def progressing(message, node: false, recovery: false)
|
|
46
|
+
renderer = ProgressRenderer.new
|
|
47
|
+
progress = Empeira::Progress.new(listener: renderer)
|
|
48
|
+
renderer.during do
|
|
49
|
+
progress.run(message) { yield application(node: node, progress: progress, recovery: recovery) }
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def runtime_overrides
|
|
54
|
+
return {} unless options[:container_engine]
|
|
55
|
+
|
|
56
|
+
{ 'runtime' => { 'container_engine' => options[:container_engine] } }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def node_overrides
|
|
60
|
+
values = options.slice('os', 'version', 'memory', 'cpus').to_h
|
|
61
|
+
%w[memory cpus].each { |key| values[key] = parse_integer(key, values[key]) if values.key?(key) }
|
|
62
|
+
{ 'node_defaults' => values }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_integer(key, value)
|
|
66
|
+
Integer(value, 10)
|
|
67
|
+
rescue ArgumentError
|
|
68
|
+
raise ConfigurationError, "node_defaults.#{key} must be a positive integer"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../completion/catalog'
|
|
4
|
+
require_relative '../completion/bash'
|
|
5
|
+
|
|
6
|
+
module Empeira
|
|
7
|
+
module CLI
|
|
8
|
+
class Completion < Base
|
|
9
|
+
desc 'bash', 'Print Bash completion for sourcing in your shell'
|
|
10
|
+
def bash
|
|
11
|
+
say Empeira::Completion::Bash.generate
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
desc 'candidates INDEX', 'Return completion candidates', hide: true
|
|
15
|
+
option :null, type: :boolean, default: false, hide: true
|
|
16
|
+
def candidates(index)
|
|
17
|
+
words = $stdin.read.split("\0", -1)
|
|
18
|
+
words.pop
|
|
19
|
+
cursor = Integer(index, 10) - 1
|
|
20
|
+
return if cursor.negative?
|
|
21
|
+
|
|
22
|
+
emit_candidates(words, cursor)
|
|
23
|
+
rescue ArgumentError
|
|
24
|
+
raise Error, 'Completion index must be an integer'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
no_commands do
|
|
28
|
+
def emit_candidates(words, cursor)
|
|
29
|
+
catalog = Empeira::Completion::Catalog.new
|
|
30
|
+
values = catalog.candidates(words, cursor)
|
|
31
|
+
addon = catalog.addon_completion
|
|
32
|
+
if addon&.file_completion
|
|
33
|
+
values += ["\x1f#{addon.file_completion}", Empeira::Completion::Bash.path_prefix(addon.file_prefix),
|
|
34
|
+
addon.replacement.to_s]
|
|
35
|
+
end
|
|
36
|
+
separator = options[:null] ? "\0" : "\n"
|
|
37
|
+
values.each { |value| $stdout.write("#{value}#{separator}") }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module CLI
|
|
5
|
+
class Config < Base
|
|
6
|
+
desc 'show', 'Display the effective configuration'
|
|
7
|
+
def show
|
|
8
|
+
app = application
|
|
9
|
+
data = Configuration::Display.redact(app.context.configuration)
|
|
10
|
+
mounts = ControlPlane::Plan.new(context: app.context).hiera.entries
|
|
11
|
+
data['effective_hiera_mounts'] = mounts unless mounts.empty?
|
|
12
|
+
say YAML.dump(data)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc 'validate', 'Validate the effective configuration'
|
|
16
|
+
def validate
|
|
17
|
+
application.context.configuration
|
|
18
|
+
say 'Configuration is valid.'
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
require_relative 'config'
|
|
5
|
+
require_relative 'node'
|
|
6
|
+
require_relative 'proxy'
|
|
7
|
+
require_relative 'completion'
|
|
8
|
+
require_relative 'update'
|
|
9
|
+
require_relative 'addons'
|
|
10
|
+
|
|
11
|
+
module Empeira
|
|
12
|
+
module CLI
|
|
13
|
+
class Main < Base
|
|
14
|
+
# Strip the opaque addon argv before Thor can interpret --help, --version,
|
|
15
|
+
# assignments, or a literal -- as Core options.
|
|
16
|
+
def self.start(given_args = ARGV, config = {})
|
|
17
|
+
index = addon_index(given_args)
|
|
18
|
+
if index && given_args[index + 1]&.match?(Configuration::Addons::NAME)
|
|
19
|
+
config = config.merge(addon_arguments: given_args.drop(index + 2))
|
|
20
|
+
given_args = [*given_args[index, 2], *given_args.take(index)]
|
|
21
|
+
end
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.addon_index(arguments)
|
|
26
|
+
index = 0
|
|
27
|
+
index += arguments[index].include?('=') ? 1 : 2 while arguments[index]&.start_with?('--container-engine')
|
|
28
|
+
index if arguments[index] == 'addon'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc 'addon NAME [ARGUMENTS...]', 'Run a configured standalone addon'
|
|
32
|
+
def addon(name)
|
|
33
|
+
application.addons.execute(name, @addon_arguments || [])
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc 'addons SUBCOMMAND', 'Manage configured Git addons'
|
|
37
|
+
subcommand 'addons', Addons
|
|
38
|
+
|
|
39
|
+
desc 'version', 'Show build version'
|
|
40
|
+
option :verbose, type: :boolean, desc: 'Include revision and build time'
|
|
41
|
+
map '--version' => :version
|
|
42
|
+
def version
|
|
43
|
+
info = BuildInfo.load
|
|
44
|
+
say(options[:verbose] ? JSON.pretty_generate(info.to_h) : "Empeira #{info.version}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
desc 'config SUBCOMMAND', 'Show or validate project configuration'
|
|
48
|
+
subcommand 'config', Config
|
|
49
|
+
desc 'proxy SUBCOMMAND', 'Explain hostname-specific proxy policy'
|
|
50
|
+
subcommand 'proxy', Proxy
|
|
51
|
+
desc 'node SUBCOMMAND', 'Container and VM test-node lifecycle'
|
|
52
|
+
subcommand 'node', Node
|
|
53
|
+
desc 'completion SUBCOMMAND', 'Shell completion'
|
|
54
|
+
subcommand 'completion', Completion
|
|
55
|
+
|
|
56
|
+
desc 'status', 'Inspect workspace infrastructure without changing state'
|
|
57
|
+
def status
|
|
58
|
+
application.infrastructure.status.each { |key, value| say "#{key}: #{value}" }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
desc 'up', 'Ensure isolated workspace infrastructure'
|
|
62
|
+
def up
|
|
63
|
+
addon_error = nil
|
|
64
|
+
result = progressing('Preparing workspace...') do |app|
|
|
65
|
+
ready = app.infrastructure.up
|
|
66
|
+
begin
|
|
67
|
+
app.addons.synchronize(missing_only: true)
|
|
68
|
+
rescue Empeira::Addons::Error => e
|
|
69
|
+
addon_error = e
|
|
70
|
+
end
|
|
71
|
+
ready
|
|
72
|
+
end
|
|
73
|
+
say(result.changed ? 'Workspace control plane is ready.' : 'Workspace control plane is already ready.')
|
|
74
|
+
raise addon_error if addon_error
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
desc 'browser', 'Start the disposable internal browser and print its loopback HTTPS URL'
|
|
78
|
+
def browser
|
|
79
|
+
url = progressing('Preparing internal browser...') { |app| app.infrastructure.browser }
|
|
80
|
+
say url
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
desc 'down', 'Remove owned workspace infrastructure'
|
|
84
|
+
def down
|
|
85
|
+
result = progressing('Stopping workspace...') { |app| app.infrastructure.down }
|
|
86
|
+
action = result.changed ? 'removed' : 'already absent'
|
|
87
|
+
say "Workspace services #{action}; persistent data retained."
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
desc 'destroy', 'Permanently remove services, CA and database volumes'
|
|
91
|
+
option :project_path, type: :string,
|
|
92
|
+
desc: 'Former absolute Git root path for recovery without a control repository'
|
|
93
|
+
def destroy
|
|
94
|
+
progressing('Removing workspace...', recovery: true) { |app| app.infrastructure.destroy }
|
|
95
|
+
say 'Workspace infrastructure and persistent data removed.'
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc 'self-update', 'Update only Empeira (not implemented yet)'
|
|
99
|
+
def self_update
|
|
100
|
+
Updates::Service.new.self_update
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
desc 'update SUBCOMMAND', 'Update managed targets (not implemented yet)'
|
|
104
|
+
subcommand 'update', Update
|
|
105
|
+
|
|
106
|
+
%w[images].each do |operation|
|
|
107
|
+
desc "#{operation} [SUBCOMMAND]", 'Not implemented yet'
|
|
108
|
+
define_method(operation) do |*_arguments|
|
|
109
|
+
raise UnavailableFeature, 'This command is not implemented yet.'
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module CLI
|
|
5
|
+
class Node < Base
|
|
6
|
+
map 'run' => :run_node, 'shell' => :shell_node, 'exec' => :exec_node
|
|
7
|
+
|
|
8
|
+
desc 'exec HOSTNAME -- COMMAND [ARGUMENTS...]', 'Run a root guest command without SSH or implicit shell parsing'
|
|
9
|
+
option :timeout, type: :numeric, desc: 'Guest command timeout in seconds (default: unlimited)'
|
|
10
|
+
def exec_node(hostname, *_arguments)
|
|
11
|
+
arguments = @guest_arguments || []
|
|
12
|
+
Empeira::Node::DiagnosticCommand.validate!(arguments, options[:timeout])
|
|
13
|
+
result = application.nodes.exec(name: hostname, arguments: arguments, timeout: options[:timeout])
|
|
14
|
+
if result.timed_out
|
|
15
|
+
warn 'Guest command timed out; its process groups were stopped.'
|
|
16
|
+
raise SystemExit, 124
|
|
17
|
+
end
|
|
18
|
+
raise SystemExit, result.exit_status
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
desc 'run HOSTNAME', 'Create a container or VM node and run its agent'
|
|
22
|
+
option :provider, type: :string, default: 'container', enum: Empeira::Node.registry.names,
|
|
23
|
+
desc: 'Node provider (default: container)'
|
|
24
|
+
option :os, type: :string, desc: 'Operating system'
|
|
25
|
+
option :version, type: :string, desc: 'Operating system version'
|
|
26
|
+
option :memory, type: :string, desc: 'Memory in MiB (positive integer)'
|
|
27
|
+
option :cpus, type: :string, desc: 'CPU count (positive integer)'
|
|
28
|
+
def run_node(hostname)
|
|
29
|
+
progressing('Preparing node...', node: true) do |app|
|
|
30
|
+
app.run_node(hostname: hostname, provider: options[:provider])
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
desc 'ssh HOSTNAME', 'Open system SSH (VM: managed identity; container: local user authentication)'
|
|
35
|
+
option :user, type: :string, desc: 'Remote login user (overrides personal SSH preferences)'
|
|
36
|
+
option :identity, type: :string, desc: 'SSH identity file (VM default: managed key)'
|
|
37
|
+
option :port, type: :string, desc: 'System SSH guest port (default: 22)'
|
|
38
|
+
def ssh(hostname)
|
|
39
|
+
result = application.nodes.ssh(name: hostname, user: options[:user], identity: options[:identity],
|
|
40
|
+
port: ssh_port)
|
|
41
|
+
raise SystemExit, result.exit_status unless result.success?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
no_commands do
|
|
45
|
+
def ssh_port
|
|
46
|
+
return unless options[:port]
|
|
47
|
+
|
|
48
|
+
Configuration::SSHPreferences.port!(Integer(options[:port], 10))
|
|
49
|
+
rescue ArgumentError
|
|
50
|
+
raise ConfigurationError, 'SSH port must be an integer from 1 to 65535'
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
%w[start stop destroy shell console puppet logs].each do |operation|
|
|
55
|
+
descriptions = { 'shell' => 'Open an interactive root terminal (runtime exec or VM VirtIO PTY)',
|
|
56
|
+
'console' => 'Attach to the private VM serial console; Ctrl+] detaches',
|
|
57
|
+
'destroy' => 'Remove an owned node without version or guest-access requirements' }
|
|
58
|
+
desc "#{operation} HOSTNAME", descriptions.fetch(operation, "#{operation.capitalize} an existing node")
|
|
59
|
+
if operation == 'destroy'
|
|
60
|
+
option :project_path, type: :string,
|
|
61
|
+
desc: 'Former absolute Git root path for recovery without a control repository'
|
|
62
|
+
end
|
|
63
|
+
define_method(operation == 'shell' ? 'shell_node' : operation) do |hostname|
|
|
64
|
+
if %w[shell console ssh logs].include?(operation)
|
|
65
|
+
result = application.nodes.public_send(operation, name: hostname)
|
|
66
|
+
raise SystemExit, result.exit_status if operation == 'ssh' && !result.success?
|
|
67
|
+
raise Error, "Node #{operation} failed" unless result.success?
|
|
68
|
+
else
|
|
69
|
+
progressing("#{operation.capitalize} node...", recovery: operation == 'destroy') do |app|
|
|
70
|
+
app.nodes.public_send(operation, name: hostname)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
desc 'list', 'List workspace nodes and observed runtime state'
|
|
77
|
+
def list
|
|
78
|
+
rows = application.nodes.list
|
|
79
|
+
say 'HOSTNAME PROVIDER RUNTIME OS/VERSION STATE PUPPET_EXIT NETWORK'
|
|
80
|
+
rows.each do |row|
|
|
81
|
+
say [row['hostname'], row['provider'], row['runtime'], "#{row['os']}/#{row['version']}",
|
|
82
|
+
row['state'], row['last_puppet_exit'] || '-', row['network'] || '-'].join(' ')
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module CLI
|
|
7
|
+
# The cursor stays on the second row while a workflow is active.
|
|
8
|
+
class ProgressDisplay
|
|
9
|
+
DEFAULT_COLUMNS = 80
|
|
10
|
+
MIN_BAR_COLUMNS = 4
|
|
11
|
+
|
|
12
|
+
def initialize(io:, columns: nil)
|
|
13
|
+
@io = io
|
|
14
|
+
@columns = columns
|
|
15
|
+
@drawn = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def render(event, waiting: false)
|
|
19
|
+
return finish unless event.state == :active
|
|
20
|
+
|
|
21
|
+
available = [terminal_columns - 1, 0].max
|
|
22
|
+
message = ProgressRenderer.clean_message(event.message)
|
|
23
|
+
message += waiting == true ? ' (still working)' : " (still working #{waiting})" if waiting
|
|
24
|
+
@io.print("\r\e[1A") if @drawn
|
|
25
|
+
@io.print("\r\e[2K", progress_line(event.percent, available), "\n\r\e[2K", truncate(message, available))
|
|
26
|
+
@drawn = true
|
|
27
|
+
@io.flush
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def finish
|
|
31
|
+
return unless @drawn
|
|
32
|
+
|
|
33
|
+
@io.print("\r\e[2K\e[1A\r\e[2K")
|
|
34
|
+
@io.flush
|
|
35
|
+
@drawn = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def progress_line(percent, available)
|
|
41
|
+
label = format('%3d%%', percent)
|
|
42
|
+
bar_width = available - label.length - 3
|
|
43
|
+
if bar_width < MIN_BAR_COLUMNS
|
|
44
|
+
compact = "[#{percent}%]"
|
|
45
|
+
compact = "#{percent}%" if compact.length > available
|
|
46
|
+
return compact.length <= available ? compact : ''
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
filled = (bar_width * percent / 100.0).round
|
|
50
|
+
"[#{'█' * filled}#{'-' * (bar_width - filled)}] #{label}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def truncate(message, available)
|
|
54
|
+
return '' if available.zero?
|
|
55
|
+
return message if message.bytesize <= available
|
|
56
|
+
|
|
57
|
+
suffix = available >= 4 ? '...' : ''
|
|
58
|
+
budget = available - suffix.length
|
|
59
|
+
result = +''
|
|
60
|
+
message.each_grapheme_cluster do |cluster|
|
|
61
|
+
break if result.bytesize + cluster.bytesize > budget
|
|
62
|
+
|
|
63
|
+
result << cluster
|
|
64
|
+
end
|
|
65
|
+
result << suffix
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def terminal_columns
|
|
69
|
+
value = @columns ? @columns.call : @io.winsize.last
|
|
70
|
+
value.is_a?(Integer) && value.positive? ? value : DEFAULT_COLUMNS
|
|
71
|
+
rescue IOError, SystemCallError, NoMethodError
|
|
72
|
+
DEFAULT_COLUMNS
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class ProgressRenderer
|
|
77
|
+
SPINNER = ['|', '/', '-', '\\'].freeze
|
|
78
|
+
SPINNER_INTERVAL = 0.25
|
|
79
|
+
|
|
80
|
+
def self.clean_message(message)
|
|
81
|
+
message.to_s.scrub.gsub(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/, ' ')
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def initialize(io: $stderr, interactive: nil, columns: nil, heartbeat_interval: 10, clock: nil)
|
|
85
|
+
@io = io
|
|
86
|
+
active = interactive.nil? ? interactive_output? : interactive
|
|
87
|
+
@display = ProgressDisplay.new(io: io, columns: columns) if active
|
|
88
|
+
@heartbeat_interval = heartbeat_interval
|
|
89
|
+
@clock = clock || -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
90
|
+
@spinner = 0
|
|
91
|
+
@mutex = Mutex.new
|
|
92
|
+
@condition = ConditionVariable.new
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def call(event)
|
|
96
|
+
@mutex.synchronize do
|
|
97
|
+
@io.print("\n") if @event&.state == :suspended && event.state == :failed
|
|
98
|
+
@event = event
|
|
99
|
+
@spinner = 0
|
|
100
|
+
@next_tick = @clock.call + @heartbeat_interval
|
|
101
|
+
render_state(event)
|
|
102
|
+
@condition.signal
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def during
|
|
107
|
+
@finished = false
|
|
108
|
+
heartbeat = Thread.new { heartbeat_loop }
|
|
109
|
+
yield
|
|
110
|
+
ensure
|
|
111
|
+
if heartbeat
|
|
112
|
+
@mutex.synchronize do
|
|
113
|
+
@finished = true
|
|
114
|
+
@condition.signal
|
|
115
|
+
end
|
|
116
|
+
heartbeat.join
|
|
117
|
+
end
|
|
118
|
+
@mutex.synchronize { @display&.finish }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def render_state(event)
|
|
124
|
+
if event.state == :warning
|
|
125
|
+
@display&.finish
|
|
126
|
+
@io.puts(self.class.clean_message(event.message))
|
|
127
|
+
@io.flush
|
|
128
|
+
elsif %i[suspended resumed].include?(event.state)
|
|
129
|
+
@display&.finish
|
|
130
|
+
@io.print("\n") if event.state == :resumed
|
|
131
|
+
else
|
|
132
|
+
render(event)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def interactive_output?
|
|
137
|
+
Execution::Terminal.interactive?(error: @io)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def heartbeat_loop
|
|
141
|
+
@mutex.synchronize do
|
|
142
|
+
until @finished
|
|
143
|
+
tick
|
|
144
|
+
delay = @event&.state == :active ? [@next_tick - @clock.call, 0].max : nil
|
|
145
|
+
@condition.wait(@mutex, delay) unless @finished
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def tick
|
|
151
|
+
return unless @event&.state == :active && @clock.call >= @next_tick
|
|
152
|
+
|
|
153
|
+
render(@event, waiting: @display ? SPINNER[@spinner % SPINNER.size] : true)
|
|
154
|
+
@spinner += 1
|
|
155
|
+
@next_tick = @clock.call + (@display ? SPINNER_INTERVAL : @heartbeat_interval)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def render(event, waiting: false)
|
|
159
|
+
return @display.render(event, waiting: waiting) if @display
|
|
160
|
+
|
|
161
|
+
message = self.class.clean_message(event.message)
|
|
162
|
+
text = format('[%<percent>3d%%] %<message>s%<wait>s', percent: event.percent, message: message,
|
|
163
|
+
wait: waiting ? ' (still working)' : '')
|
|
164
|
+
@io.puts(text)
|
|
165
|
+
@io.flush
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Empeira
|
|
4
|
+
module CLI
|
|
5
|
+
class Proxy < Base
|
|
6
|
+
desc 'show HOSTNAME', 'Explain the normal proxy destination policy for a hostname'
|
|
7
|
+
def show(hostname)
|
|
8
|
+
config = application.context.configuration.fetch('proxy')
|
|
9
|
+
resolver = Network::HostPolicy.new(config)
|
|
10
|
+
say "Effective proxy policy for #{hostname.downcase}"
|
|
11
|
+
say access_message(config)
|
|
12
|
+
display_list('Global', config.fetch('global'))
|
|
13
|
+
display_matches(resolver.matching(hostname))
|
|
14
|
+
display_list('Allowed', resolver.resolve(hostname))
|
|
15
|
+
say 'Managed bootstrap destinations are separate from this normal policy.'
|
|
16
|
+
end
|
|
17
|
+
no_commands do
|
|
18
|
+
def access_message(config)
|
|
19
|
+
return "All workspace nodes may use this policy after 'empeira up' reconciles the proxy." if
|
|
20
|
+
config.fetch('enabled')
|
|
21
|
+
|
|
22
|
+
'The normal workspace proxy is disabled.'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def display_matches(rules)
|
|
26
|
+
display_list('Matched rules', rules.map { |rule| rule.fetch('hosts').join(', ') })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def display_list(title, entries)
|
|
30
|
+
say "#{title}:"
|
|
31
|
+
entries.each { |entry| say " #{entry}" }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Empeira
|
|
6
|
+
module CLI
|
|
7
|
+
class Update < Base
|
|
8
|
+
Updates::Service::TARGETS.each do |target|
|
|
9
|
+
desc target,
|
|
10
|
+
{ 'all' => 'Update all managed targets, including Empeira (unavailable)',
|
|
11
|
+
'modules' => 'Synchronize Puppetfile module artifacts',
|
|
12
|
+
'images' => 'Refresh configured container image artifacts' }.fetch(target)
|
|
13
|
+
define_method(target) do
|
|
14
|
+
if target == 'all'
|
|
15
|
+
application.updates.update(target)
|
|
16
|
+
else
|
|
17
|
+
progressing("Updating #{target}...") { |app| app.updates.update(target) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|