moby-derp 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/.gitignore +6 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTING.md +14 -0
- data/LICENCE +674 -0
- data/README.md +231 -0
- data/bin/moby-derp +56 -0
- data/example.yml +297 -0
- data/lib/freedom_patches/docker/image.rb +23 -0
- data/lib/moby_derp/config_file.rb +33 -0
- data/lib/moby_derp/container.rb +245 -0
- data/lib/moby_derp/container_config.rb +377 -0
- data/lib/moby_derp/error.rb +15 -0
- data/lib/moby_derp/logging_helpers.rb +26 -0
- data/lib/moby_derp/moby_info.rb +12 -0
- data/lib/moby_derp/mount.rb +56 -0
- data/lib/moby_derp/pod.rb +89 -0
- data/lib/moby_derp/pod_config.rb +235 -0
- data/lib/moby_derp/system_config.rb +52 -0
- data/moby-derp.gemspec +39 -0
- data/smoke_tests/minimal.bats +18 -0
- data/smoke_tests/no_file.bats +7 -0
- data/smoke_tests/test_helper.bash +29 -0
- metadata +247 -0
@@ -0,0 +1,245 @@
|
|
1
|
+
require_relative "./logging_helpers"
|
2
|
+
|
3
|
+
require "digest/sha2"
|
4
|
+
require "docker-api"
|
5
|
+
require "ipaddr"
|
6
|
+
require "json/canonicalization"
|
7
|
+
|
8
|
+
module MobyDerp
|
9
|
+
class Container
|
10
|
+
include LoggingHelpers
|
11
|
+
|
12
|
+
def initialize(pod:, container_config:, root_container: false)
|
13
|
+
@logger = pod.logger
|
14
|
+
|
15
|
+
@pod, @config, @root_container = pod, container_config, root_container
|
16
|
+
end
|
17
|
+
|
18
|
+
def run
|
19
|
+
container_name = @root_container ? @pod.name : @config.name
|
20
|
+
@logger.debug(logloc) { "Calculated container name is #{container_name} (@root_container: #{@root_container.inspect}, @config.name: #{@config.name}, @pod.name: #{@pod.name}" }
|
21
|
+
|
22
|
+
begin
|
23
|
+
existing_container = Docker::Container.get(container_name)
|
24
|
+
@logger.debug(logloc) { "Config hash for existing container #{container_name} is #{existing_container.info["Config"]["Labels"]["org.hezmatt.moby-derp.config-hash"].inspect}" }
|
25
|
+
@logger.debug(logloc) { "New config hash is #{params_hash(container_creation_parameters).inspect}" }
|
26
|
+
|
27
|
+
if existing_container.info["Config"]["Labels"]["org.hezmatt.moby-derp.config-hash"] == params_hash(container_creation_parameters)
|
28
|
+
# Container is up-to-date
|
29
|
+
@logger.info(logloc) { "Container #{container_name} is up-to-date" }
|
30
|
+
return existing_container.id
|
31
|
+
end
|
32
|
+
|
33
|
+
if existing_container.info["Config"]["Labels"]["org.hezmatt.moby-derp.pod-name"] != @pod.name
|
34
|
+
raise ContainerError,
|
35
|
+
"container #{container_name} is not tagged as being part of this pod"
|
36
|
+
end
|
37
|
+
@logger.info(logloc) { "Deleting container #{container_name} (#{existing_container.id[0..11]}) because it is out-of-date" }
|
38
|
+
existing_container.delete(force: true)
|
39
|
+
@logger.info(logloc) { "Creating new container #{container_name}" }
|
40
|
+
rescue Docker::Error::NotFoundError
|
41
|
+
@logger.info(logloc) { "Container #{container_name} does not exist; creating it" }
|
42
|
+
# Container doesn't exist, need to create it
|
43
|
+
end
|
44
|
+
|
45
|
+
begin
|
46
|
+
Docker::Container.create(hash_labelled(container_creation_parameters)).start!.id
|
47
|
+
rescue Docker::Error::ClientError => ex
|
48
|
+
raise MobyDerp::ContainerError,
|
49
|
+
"moby daemon returned error: #{ex.message}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def container_creation_parameters
|
56
|
+
{}.tap do |params|
|
57
|
+
if @root_container
|
58
|
+
params["HostConfig"] = {
|
59
|
+
"NetworkMode" => @pod.network_name,
|
60
|
+
"Init" => true,
|
61
|
+
}
|
62
|
+
params["MacAddress"] = container_mac_address
|
63
|
+
if network_uses_ipv6?
|
64
|
+
params["NetworkingConfig"] = {
|
65
|
+
"EndpointsConfig" => {
|
66
|
+
@pod.network_name => {
|
67
|
+
"IPAMConfig" => {
|
68
|
+
"IPv6Address" => container_ipv6_address,
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
}
|
73
|
+
end
|
74
|
+
else
|
75
|
+
params["HostConfig"] = {
|
76
|
+
"NetworkMode" => "container:#{@pod.root_container_id}",
|
77
|
+
"PidMode" => "container:#{@pod.root_container_id}",
|
78
|
+
"IpcMode" => "container:#{@pod.root_container_id}",
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
params["HostConfig"]["RestartPolicy"] = parsed_restart_policy
|
83
|
+
params["HostConfig"]["Mounts"] = merged_mounts.map { |mount| mount_structure(mount) }
|
84
|
+
|
85
|
+
params["Env"] = @pod.common_environment.merge(@config.environment).map { |k, v| "#{k}=#{v}" }
|
86
|
+
params["Volumes"] = {}
|
87
|
+
|
88
|
+
params["name"] = @root_container ? @pod.name : @config.name
|
89
|
+
params["Image"] = image_id
|
90
|
+
|
91
|
+
params["Cmd"] = @config.command
|
92
|
+
params["StopSignal"] = @config.stop_signal
|
93
|
+
params["StopTimeout"] = @config.stop_timeout
|
94
|
+
|
95
|
+
if @config.readonly
|
96
|
+
params["HostConfig"]["ReadonlyRootfs"] = true
|
97
|
+
end
|
98
|
+
|
99
|
+
if @config.limits["cpus"]
|
100
|
+
params["HostConfig"]["NanoCPUs"] = @config.limits["cpus"] * 10 ** 9
|
101
|
+
end
|
102
|
+
|
103
|
+
{
|
104
|
+
"cpu-shares" => "CpuShares",
|
105
|
+
"oom-score-adj" => "OomScoreAdj",
|
106
|
+
"pids" => "PidsLimit",
|
107
|
+
"memory" => "Memory",
|
108
|
+
"memory-swap" => "MemorySwap",
|
109
|
+
"memory-reservation" => "MemoryReservation",
|
110
|
+
"shm-size" => "ShmSize",
|
111
|
+
}.each do |limit_name, moby_limit_name|
|
112
|
+
if @config.limits[limit_name]
|
113
|
+
params["HostConfig"][moby_limit_name] = @config.limits[limit_name]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
@config.limits.keys.grep(/^ulimit-/).each do |ulimit|
|
118
|
+
params["HostConfig"]["Ulimits"] ||= []
|
119
|
+
params["HostConfig"]["Ulimits"] << ulimit_structure(ulimit)
|
120
|
+
end
|
121
|
+
|
122
|
+
params["Labels"] = @pod.common_labels.merge(@config.labels)
|
123
|
+
params["Labels"]["org.hezmatt.moby-derp.pod-name"] = @pod.name
|
124
|
+
|
125
|
+
unless @root_container
|
126
|
+
params["Labels"]["org.hezmatt.moby-derp.root-container-id"] = @pod.root_container_id
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def hash_labelled(params)
|
132
|
+
params.tap do |params|
|
133
|
+
config_hash = params_hash(params)
|
134
|
+
|
135
|
+
params["Labels"] ||= {}
|
136
|
+
params["Labels"]["org.hezmatt.moby-derp.config-hash"] = config_hash
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def params_hash(params)
|
141
|
+
"sha256:#{Digest::SHA256.hexdigest(params.to_json_c14n)}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def image_id
|
145
|
+
if @config.image =~ /\A#{Docker::Image::DIGEST}\z/
|
146
|
+
@config.image
|
147
|
+
else
|
148
|
+
if @config.update_image
|
149
|
+
begin
|
150
|
+
Docker::Image.create(fromImage: @config.image).id
|
151
|
+
rescue Docker::Error::NotFoundError
|
152
|
+
raise ContainerError,
|
153
|
+
"image #{@config.image} for container #{@config.name} cannot be downloaded"
|
154
|
+
end
|
155
|
+
else
|
156
|
+
begin
|
157
|
+
Docker::Image.get(@config.image).id
|
158
|
+
rescue Docker::Error::NotFoundError
|
159
|
+
# Image doesn't exist locally, so we'll have to pull it after all
|
160
|
+
begin
|
161
|
+
Docker::Image.create(fromImage: @config.image).id
|
162
|
+
rescue Docker::Error::NotFoundError
|
163
|
+
raise ContainerError,
|
164
|
+
"image #{@config.image} for container #{@config.name} cannot be downloaded"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def parsed_restart_policy
|
172
|
+
@config.restart =~ /\A([a-z-]+)(:(\d+))?\z/
|
173
|
+
{ "Name" => $1 }.tap do |policy|
|
174
|
+
if $3
|
175
|
+
policy["MaximumRetryCount"] = $3.to_i
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def merged_mounts
|
181
|
+
container_mount_targets = @config.mounts.map { |m| m.target }
|
182
|
+
|
183
|
+
@config.mounts + @pod.common_mounts.select { |m| !container_mount_targets.include?(m.target) }
|
184
|
+
end
|
185
|
+
|
186
|
+
def mount_structure(mount)
|
187
|
+
{
|
188
|
+
"Type" => "bind",
|
189
|
+
"Source" => "#{@pod.mount_root}/#{mount.source}",
|
190
|
+
"Target" => mount.target,
|
191
|
+
"ReadOnly" => mount.readonly,
|
192
|
+
}
|
193
|
+
end
|
194
|
+
|
195
|
+
def ulimit_structure(limit_key)
|
196
|
+
{
|
197
|
+
"Name" => limit_key.sub(/^ulimit-/, ''),
|
198
|
+
"Soft" => @config.limits[limit_key].first,
|
199
|
+
"Hard" => @config.limits[limit_key].last,
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
def container_mac_address
|
204
|
+
"02:" + Digest::SHA256.hexdigest(@pod.name + Socket.gethostname)[0..9].scan(/../).join(":")
|
205
|
+
end
|
206
|
+
|
207
|
+
def docker_network
|
208
|
+
begin
|
209
|
+
network = Docker::Network.get(@pod.network_name)
|
210
|
+
rescue Docker::Error::NotFoundError
|
211
|
+
raise ContainerError,
|
212
|
+
"network #{@pod.network_name} does not exist"
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def network_uses_ipv6?
|
217
|
+
docker_network.info["EnableIPv6"]
|
218
|
+
end
|
219
|
+
|
220
|
+
def container_ipv6_address
|
221
|
+
network, masklen = ipv6_network.split("/", 2)
|
222
|
+
network = IPAddr.new(network)
|
223
|
+
masklen = masklen.to_i
|
224
|
+
|
225
|
+
(network | Digest::SHA256.hexdigest(container_mac_address).to_i(16) % 2**masklen).to_s
|
226
|
+
end
|
227
|
+
|
228
|
+
def ipv6_network
|
229
|
+
ipam = docker_network.info["IPAM"]
|
230
|
+
unless ipam["Driver"] == "default"
|
231
|
+
raise ContainerError,
|
232
|
+
"Unsupported IPAM driver #{ipam["Driver"]} on network #{@pod.network_name}"
|
233
|
+
end
|
234
|
+
|
235
|
+
begin
|
236
|
+
ipam["Config"].find do |cfg|
|
237
|
+
IPAddr.new(cfg["Subnet"]).ipv6?
|
238
|
+
end["Subnet"]
|
239
|
+
rescue NoMethodError
|
240
|
+
raise ContainerError,
|
241
|
+
"No IPv6 subnet found on network #{@pod.network_name}"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,377 @@
|
|
1
|
+
require_relative "../freedom_patches/docker/image"
|
2
|
+
require_relative "./error"
|
3
|
+
require_relative "./mount"
|
4
|
+
|
5
|
+
require "docker-api"
|
6
|
+
|
7
|
+
module MobyDerp
|
8
|
+
class ContainerConfig
|
9
|
+
attr_reader :name, :image, :update_image, :command, :environment, :mounts,
|
10
|
+
:labels, :readonly, :stop_signal, :stop_timeout, :restart, :limits
|
11
|
+
|
12
|
+
def initialize(system_config:,
|
13
|
+
pod_config:,
|
14
|
+
container_name:,
|
15
|
+
image:,
|
16
|
+
update_image: true,
|
17
|
+
command: [],
|
18
|
+
environment: {},
|
19
|
+
mounts: [],
|
20
|
+
labels: {},
|
21
|
+
readonly: false,
|
22
|
+
stop_signal: "SIGTERM",
|
23
|
+
stop_timeout: 10,
|
24
|
+
restart: "no",
|
25
|
+
limits: {}
|
26
|
+
)
|
27
|
+
@system_config, @pod_config, @name, @image = system_config, pod_config, "#{pod_config.name}.#{container_name}", image
|
28
|
+
|
29
|
+
@update_image, @command, @environment, @mounts, @labels = update_image, command, environment, mounts, labels
|
30
|
+
@readonly, @stop_signal, @stop_timeout, @restart = readonly, stop_signal, stop_timeout, restart
|
31
|
+
@limits = limits
|
32
|
+
|
33
|
+
validate_image
|
34
|
+
validate_update_image
|
35
|
+
validate_command
|
36
|
+
validate_environment
|
37
|
+
validate_mounts
|
38
|
+
validate_labels
|
39
|
+
validate_readonly
|
40
|
+
validate_stop_signal
|
41
|
+
validate_stop_timeout
|
42
|
+
validate_restart
|
43
|
+
validate_limits
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def validate_image
|
49
|
+
unless @image.is_a?(String)
|
50
|
+
raise ConfigurationError,
|
51
|
+
"image must be a string"
|
52
|
+
end
|
53
|
+
|
54
|
+
unless @image =~ Docker::Image::IMAGE_REFERENCE
|
55
|
+
raise ConfigurationError,
|
56
|
+
"image is not a valid image reference"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def validate_update_image
|
61
|
+
validate_boolean(:update_image)
|
62
|
+
end
|
63
|
+
|
64
|
+
def validate_command
|
65
|
+
case @command
|
66
|
+
when String
|
67
|
+
true
|
68
|
+
when Array
|
69
|
+
unless @command.all? { |c| String === c }
|
70
|
+
raise ConfigurationError, "all elements of the command array must be strings"
|
71
|
+
end
|
72
|
+
else
|
73
|
+
raise ConfigurationError,
|
74
|
+
"command must be string or array of strings"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def validate_environment
|
79
|
+
validate_hash(:environment)
|
80
|
+
|
81
|
+
if (bad_vars = @environment.keys.select { |k| k =~ /=/ }) != []
|
82
|
+
raise ConfigurationError,
|
83
|
+
"environment variable names cannot include equals signs: #{bad_vars.inspect}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def validate_mounts
|
88
|
+
unless @mounts.is_a?(Array)
|
89
|
+
raise ConfigurationError,
|
90
|
+
"mounts must be an array"
|
91
|
+
end
|
92
|
+
|
93
|
+
begin
|
94
|
+
@mounts.map! { |m| Mount.new(**symbolize_keys(m)) }
|
95
|
+
rescue ArgumentError => ex
|
96
|
+
case ex.message
|
97
|
+
when /unknown keywords?: (.*)$/
|
98
|
+
raise ConfigurationError,
|
99
|
+
"unknown mount option(s): #{$1}"
|
100
|
+
when /missing keywords?: (.*)$/
|
101
|
+
raise ConfigurationError,
|
102
|
+
"missing mount option(s): #{$1}"
|
103
|
+
else
|
104
|
+
#:nocov:
|
105
|
+
raise
|
106
|
+
#:nocov:
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def validate_labels
|
112
|
+
validate_hash(:labels)
|
113
|
+
end
|
114
|
+
|
115
|
+
def validate_readonly
|
116
|
+
validate_boolean(:readonly)
|
117
|
+
end
|
118
|
+
|
119
|
+
def validate_stop_signal
|
120
|
+
if @stop_signal.is_a?(String)
|
121
|
+
signame = @stop_signal.sub(/\ASIG/, "")
|
122
|
+
# This is not 100% accurate, because in theory moby-derp could
|
123
|
+
# be running on a different platform to the Moby server it is
|
124
|
+
# controlling, but we'll worry about that if we ever come to it.
|
125
|
+
unless Signal.list.has_key?(signame)
|
126
|
+
raise ConfigurationError,
|
127
|
+
"unknown signal name: #{@stop_signal.inspect}"
|
128
|
+
end
|
129
|
+
elsif @stop_signal.is_a?(Integer)
|
130
|
+
unless Signal.list.values.include?(@stop_signal)
|
131
|
+
raise ConfigurationError,
|
132
|
+
"unknown signal ID #{@stop_signal}"
|
133
|
+
end
|
134
|
+
else
|
135
|
+
raise ConfigurationError,
|
136
|
+
"stop_signal must be a string or integer"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def validate_stop_timeout
|
141
|
+
unless @stop_timeout.is_a?(Integer)
|
142
|
+
raise ConfigurationError,
|
143
|
+
"stop_timeout must be an integer"
|
144
|
+
end
|
145
|
+
|
146
|
+
if @stop_timeout < 0
|
147
|
+
raise ConfigurationError,
|
148
|
+
"stop_timeout cannot be negative"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def validate_restart
|
153
|
+
unless @restart.is_a?(String)
|
154
|
+
raise ConfigurationError,
|
155
|
+
"restart must be a string"
|
156
|
+
end
|
157
|
+
|
158
|
+
unless @restart =~ /\Ano|on-failure(:\d+)?|always|unless-stopped\z/
|
159
|
+
raise ConfigurationError,
|
160
|
+
"invalid value for restart parameter"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
KNOWN_LIMITS = %w{
|
165
|
+
cpus cpu-shares memory memory-swap memory-reservation oom-score-adj
|
166
|
+
pids shm-size ulimit-core ulimit-cpu ulimit-data ulimit-fsize
|
167
|
+
ulimit-memlock ulimit-msgqueue ulimit-nofile ulimit-rttime ulimit-stack
|
168
|
+
}
|
169
|
+
|
170
|
+
def validate_limits
|
171
|
+
unless @limits.is_a?(Hash)
|
172
|
+
raise ConfigurationError,
|
173
|
+
"limits must be a map"
|
174
|
+
end
|
175
|
+
|
176
|
+
if (bad_keys = @limits.keys - KNOWN_LIMITS) != []
|
177
|
+
raise ConfigurationError,
|
178
|
+
"unknown limit(s): #{bad_keys.inspect}"
|
179
|
+
end
|
180
|
+
|
181
|
+
validate_cpus_limit
|
182
|
+
validate_cpushares_limit
|
183
|
+
validate_memory_limit
|
184
|
+
validate_memoryswap_limit
|
185
|
+
validate_memoryreservation_limit
|
186
|
+
validate_oomscoreadj_limit
|
187
|
+
validate_pids_limit
|
188
|
+
validate_shmsize_limit
|
189
|
+
validate_ulimits
|
190
|
+
end
|
191
|
+
|
192
|
+
def validate_cpus_limit
|
193
|
+
return unless @limits.has_key?("cpus")
|
194
|
+
|
195
|
+
unless @limits["cpus"].is_a?(Numeric)
|
196
|
+
raise ConfigurationError,
|
197
|
+
"cpus limit must be a number"
|
198
|
+
end
|
199
|
+
|
200
|
+
if @limits["cpus"] <= 0
|
201
|
+
raise ConfigurationError,
|
202
|
+
"cpus limit must be a positive number"
|
203
|
+
end
|
204
|
+
|
205
|
+
if @limits["cpus"] > @system_config.cpu_count
|
206
|
+
raise ConfigurationError,
|
207
|
+
"cannot use #{@limits["cpus"]}, as the system only has #{@system_config.cpu_count} CPUs"
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def validate_cpushares_limit
|
212
|
+
return unless @limits.has_key?("cpu-shares")
|
213
|
+
|
214
|
+
unless @limits["cpu-shares"].is_a?(Integer)
|
215
|
+
raise ConfigurationError,
|
216
|
+
"cpu-shares limit must be an integer"
|
217
|
+
end
|
218
|
+
|
219
|
+
unless (2..1024).include?(@limits["cpu-shares"])
|
220
|
+
raise ConfigurationError,
|
221
|
+
"cpu-shares limit must be an integer between 2 and 1024 inclusive"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def validate_memory_limit
|
226
|
+
validate_memory_type_limit("memory")
|
227
|
+
end
|
228
|
+
|
229
|
+
def validate_memoryswap_limit
|
230
|
+
validate_memory_type_limit("memory-swap")
|
231
|
+
end
|
232
|
+
|
233
|
+
def validate_memoryreservation_limit
|
234
|
+
validate_memory_type_limit("memory-reservation")
|
235
|
+
end
|
236
|
+
|
237
|
+
def validate_oomscoreadj_limit
|
238
|
+
return unless @limits.has_key?("oom-score-adj")
|
239
|
+
|
240
|
+
unless @limits["oom-score-adj"].is_a?(Integer)
|
241
|
+
raise ConfigurationError,
|
242
|
+
"oom-score-adj limit must be an integer"
|
243
|
+
end
|
244
|
+
|
245
|
+
unless (0..1000).include?(@limits["oom-score-adj"])
|
246
|
+
raise ConfigurationError,
|
247
|
+
"oom-score-adj limit must be an integer between 0 and 1000 inclusive"
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def validate_pids_limit
|
252
|
+
return unless @limits.has_key?("pids")
|
253
|
+
|
254
|
+
unless @limits["pids"].is_a?(Integer)
|
255
|
+
raise ConfigurationError,
|
256
|
+
"pids limit must be an integer"
|
257
|
+
end
|
258
|
+
|
259
|
+
# As far as I can see, the only 32-bit platform that Moby supports is
|
260
|
+
# armhf. Extend the list if required.
|
261
|
+
max_pids = @system_config.cpu_bits == 32 ? 2**15 : 2**22
|
262
|
+
|
263
|
+
unless (-1..max_pids).include?(@limits["pids"])
|
264
|
+
raise ConfigurationError,
|
265
|
+
"pids limit must be an integer between -1 and #{max_pids} inclusive"
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def validate_shmsize_limit
|
270
|
+
validate_memory_type_limit("shm-size")
|
271
|
+
end
|
272
|
+
|
273
|
+
def validate_ulimits
|
274
|
+
@limits.keys.grep(/\Aulimit-.*\z/).each do |ulimit|
|
275
|
+
unless @limits[ulimit] =~ /\A(unlimited|\d+)(:(unlimited|\d+))?\z/
|
276
|
+
raise ConfigurationError,
|
277
|
+
"invalid limit syntax for #{ulimit}: must be <softlimit>[:<hardlimit>]"
|
278
|
+
end
|
279
|
+
|
280
|
+
@limits[ulimit] = [ulimit_value($1)]
|
281
|
+
|
282
|
+
if $2.nil?
|
283
|
+
@limits[ulimit][1] = @limits[ulimit][0]
|
284
|
+
else
|
285
|
+
@limits[ulimit][1] = ulimit_value($3)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
def ulimit_value(s)
|
291
|
+
if s == "unlimited"
|
292
|
+
-1
|
293
|
+
else
|
294
|
+
s.to_i
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
def validate_memory_type_limit(name)
|
299
|
+
return unless @limits.has_key?(name)
|
300
|
+
|
301
|
+
case @limits[name]
|
302
|
+
when Integer
|
303
|
+
if @limits[name] < 0
|
304
|
+
raise ConfigurationError,
|
305
|
+
"#{name} limit must not be a negative number"
|
306
|
+
end
|
307
|
+
when String
|
308
|
+
unless @limits[name] =~ /\A(\d+(\.\d+)?)([kKmMgGtTpP]?)[bB]?\z/
|
309
|
+
raise ConfigurationError,
|
310
|
+
"invalid value for #{name} limit: #{@limits[name]}"
|
311
|
+
end
|
312
|
+
@limits[name] = ($1.to_f * multiplier($3)).to_i
|
313
|
+
else
|
314
|
+
raise ConfigurationError,
|
315
|
+
"#{name} limit must be a string or an integer"
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def validate_boolean(name)
|
320
|
+
v = instance_variable_get(:"@#{name}")
|
321
|
+
unless v == true || v == false
|
322
|
+
raise ConfigurationError,
|
323
|
+
"#{name} setting must be a boolean"
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def validate_hash(name)
|
328
|
+
h = instance_variable_get(:"@#{name}")
|
329
|
+
|
330
|
+
unless h.is_a?(Hash)
|
331
|
+
raise ConfigurationError,
|
332
|
+
"#{h} is not a map"
|
333
|
+
end
|
334
|
+
|
335
|
+
unless (bad_keys = h.keys.select { |k| !k.is_a?(String) }) == []
|
336
|
+
raise ConfigurationError,
|
337
|
+
"#{h} contains non-string key(s): #{bad_keys.inspect}"
|
338
|
+
end
|
339
|
+
|
340
|
+
unless (bad_values = h.values.select { |v| !v.is_a?(String) }) == []
|
341
|
+
raise ConfigurationError,
|
342
|
+
"#{h} contains non-string value(s): #{bad_values.inspect}"
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
346
|
+
def symbolize_keys(h)
|
347
|
+
{}.tap do |res|
|
348
|
+
h.keys.each do |k|
|
349
|
+
res[k.to_sym] = h[k]
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
|
354
|
+
def multiplier(s)
|
355
|
+
case s.upcase
|
356
|
+
when ''
|
357
|
+
1
|
358
|
+
when 'K'
|
359
|
+
1024
|
360
|
+
when 'M'
|
361
|
+
1024 * 1024
|
362
|
+
when 'G'
|
363
|
+
1024 * 1024 * 1024
|
364
|
+
when 'T'
|
365
|
+
1024 * 1024 * 1024 * 1024
|
366
|
+
when 'P'
|
367
|
+
1024 * 1024 * 1024 * 1024 * 1024
|
368
|
+
else
|
369
|
+
#:nocov:
|
370
|
+
raise ConfigurationError,
|
371
|
+
"Unknown suffix #{s.inspect}"
|
372
|
+
#:nocov:
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
end
|
377
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module MobyDerp
|
2
|
+
# Base class for all MobyDerp-specific errors
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# Raised when something isn't quite right with the system or pod
|
6
|
+
# configuration
|
7
|
+
class ConfigurationError < Error; end
|
8
|
+
|
9
|
+
# Indicates there was a problem manipulating a live container
|
10
|
+
class ContainerError < Error; end
|
11
|
+
|
12
|
+
# Only appears when an inviolable assertion is invalid, and indicates
|
13
|
+
# there is a bug in the code
|
14
|
+
class BugError < Error; end
|
15
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MobyDerp
|
2
|
+
module LoggingHelpers
|
3
|
+
private
|
4
|
+
|
5
|
+
def log_exception(ex, progname = nil)
|
6
|
+
#:nocov:
|
7
|
+
progname ||= "#{self.class.to_s}##{caller_locations(2, 1).first.label}"
|
8
|
+
|
9
|
+
logger.error(progname) do
|
10
|
+
explanation = if block_given?
|
11
|
+
yield
|
12
|
+
else
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
(["#{explanation}#{explanation ? ": " : ""}#{ex.message} (#{ex.class})"] + ex.backtrace).join("\n ")
|
17
|
+
end
|
18
|
+
#:nocov:
|
19
|
+
end
|
20
|
+
|
21
|
+
def logloc
|
22
|
+
loc = caller_locations.first
|
23
|
+
"#{self.class}##{loc.label}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module MobyDerp
|
2
|
+
class MobyInfo
|
3
|
+
attr_reader :cpu_count, :cpu_bits
|
4
|
+
|
5
|
+
def initialize(info)
|
6
|
+
@cpu_count = info["NCPU"]
|
7
|
+
# As far as I can tell, the only 32-bit platform Moby supports is
|
8
|
+
# armhf; if that turns out to be incorrect, amend the list below.
|
9
|
+
@cpu_bits = %w{armhf}.include?(info["Architecture"]) ? 32 : 64
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|