vagrant-lxc 1.0.0.alpha.2 → 1.0.0.alpha.3
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 +26 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +26 -31
- data/README.md +4 -4
- data/lib/vagrant-lxc/action.rb +8 -2
- data/lib/vagrant-lxc/action/fetch_ip_with_lxc_attach.rb +1 -1
- data/lib/vagrant-lxc/command/sudoers.rb +18 -119
- data/lib/vagrant-lxc/driver.rb +6 -4
- data/lib/vagrant-lxc/driver/cli.rb +19 -11
- data/lib/vagrant-lxc/sudo_wrapper.rb +2 -0
- data/lib/vagrant-lxc/version.rb +1 -1
- data/spec/unit/driver/cli_spec.rb +57 -12
- data/spec/unit/driver_spec.rb +24 -12
- data/templates/sudoers.rb.erb +110 -0
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bd50ec26aecfabf94b58e9f16ff0a996fc6d7598
|
|
4
|
+
data.tar.gz: ae1c22bb9caf6b7b233d0acc100437619f3b5728
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1235a0aa274186d0181eadda1f1491ecde4484f96dfc1f50a6b313621da5cd35ce06f75d43dc4a74281c27e55aabc65949187c7abe3f484a9e574f191b8fe536
|
|
7
|
+
data.tar.gz: 91a879e4104ea0c7e2104c36e91147037ba76f0a3c37e79e7cc9f2e4e3a5ec279b39d6b99f84f12399bae2c24ec38ed8a2dab47957f4dc356b0bb8d36e075f85
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,29 @@
|
|
|
1
|
+
## [1.0.0.alpha.3](https://github.com/fgrehm/vagrant-lxc/compare/v1.0.0.alpha.2...v1.0.0.alpha.3) (Aug 9, 2014)
|
|
2
|
+
|
|
3
|
+
IMPROVEMENTS:
|
|
4
|
+
|
|
5
|
+
- Remove `lxc-shutdown` usage in favor of Vagrant's built in graceful halt
|
|
6
|
+
- Add fallback mechanism for platforms without `lxc-attach` support [[GH-294]]
|
|
7
|
+
|
|
8
|
+
[GH-294]: https://github.com/fgrehm/vagrant-lxc/pull/294
|
|
9
|
+
|
|
10
|
+
BUG FIXES:
|
|
11
|
+
|
|
12
|
+
- Figure out the real executable paths for whitelisted commands on the sudo
|
|
13
|
+
wrapper script instead of hardcoding Ubuntu paths [[GH-304]] / [[GH-305]]
|
|
14
|
+
- Attach to containers using the `MOUNT` namespace when attempting to fetch
|
|
15
|
+
container's IP [[GH-300]]
|
|
16
|
+
- Escape space characters for synced folders [[GH-291]]
|
|
17
|
+
- Use Vagrant's ruby on the sudoers file so that it works on systems that don't
|
|
18
|
+
have a global ruby installation [[GH-289]]
|
|
19
|
+
|
|
20
|
+
[GH-304]: https://github.com/fgrehm/vagrant-lxc/issues/304
|
|
21
|
+
[GH-305]: https://github.com/fgrehm/vagrant-lxc/issues/305
|
|
22
|
+
[GH-300]: https://github.com/fgrehm/vagrant-lxc/issues/300
|
|
23
|
+
[GH-291]: https://github.com/fgrehm/vagrant-lxc/issues/291
|
|
24
|
+
[GH-289]: https://github.com/fgrehm/vagrant-lxc/issues/289
|
|
25
|
+
|
|
26
|
+
|
|
1
27
|
## [1.0.0.alpha.2](https://github.com/fgrehm/vagrant-lxc/compare/v1.0.0.alpha.1...v1.0.0.alpha.2) (May 13, 2014)
|
|
2
28
|
|
|
3
29
|
BACKWARDS INCOMPATIBILITIES:
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
GIT
|
|
2
2
|
remote: https://github.com/fgrehm/vagrant-cachier.git
|
|
3
|
-
revision:
|
|
3
|
+
revision: 6f275353b82ab57ada04c79f6fb2befce0395c77
|
|
4
4
|
specs:
|
|
5
5
|
vagrant-cachier (0.7.2)
|
|
6
6
|
|
|
@@ -22,9 +22,9 @@ GIT
|
|
|
22
22
|
|
|
23
23
|
GIT
|
|
24
24
|
remote: https://github.com/mitchellh/vagrant.git
|
|
25
|
-
revision:
|
|
25
|
+
revision: 0a5f6bb77e6a61b56c96057de94f5f8c6868e27c
|
|
26
26
|
specs:
|
|
27
|
-
vagrant (1.6.
|
|
27
|
+
vagrant (1.6.4.dev)
|
|
28
28
|
bundler (>= 1.5.2, < 1.7.0)
|
|
29
29
|
childprocess (~> 0.5.0)
|
|
30
30
|
erubis (~> 2.7.0)
|
|
@@ -40,7 +40,7 @@ GIT
|
|
|
40
40
|
PATH
|
|
41
41
|
remote: .
|
|
42
42
|
specs:
|
|
43
|
-
vagrant-lxc (1.0.0.alpha.
|
|
43
|
+
vagrant-lxc (1.0.0.alpha.3)
|
|
44
44
|
|
|
45
45
|
GEM
|
|
46
46
|
remote: https://rubygems.org/
|
|
@@ -51,9 +51,6 @@ GEM
|
|
|
51
51
|
builder (3.2.2)
|
|
52
52
|
celluloid (0.15.2)
|
|
53
53
|
timers (~> 1.1.0)
|
|
54
|
-
celluloid-io (0.15.0)
|
|
55
|
-
celluloid (>= 0.15.0)
|
|
56
|
-
nio4r (>= 0.5.0)
|
|
57
54
|
childprocess (0.5.3)
|
|
58
55
|
ffi (~> 1.0, >= 1.0.11)
|
|
59
56
|
coderay (1.1.0)
|
|
@@ -67,7 +64,7 @@ GEM
|
|
|
67
64
|
docile (1.1.3)
|
|
68
65
|
erubis (2.7.0)
|
|
69
66
|
ffi (1.9.3)
|
|
70
|
-
formatador (0.2.
|
|
67
|
+
formatador (0.2.5)
|
|
71
68
|
gssapi (1.0.3)
|
|
72
69
|
ffi (>= 1.0.1)
|
|
73
70
|
guard (2.6.1)
|
|
@@ -81,13 +78,12 @@ GEM
|
|
|
81
78
|
rspec (>= 2.14, < 4.0)
|
|
82
79
|
gyoku (1.1.1)
|
|
83
80
|
builder (>= 2.1.2)
|
|
84
|
-
httpclient (2.
|
|
81
|
+
httpclient (2.4.0)
|
|
85
82
|
httpi (0.9.7)
|
|
86
83
|
rack
|
|
87
84
|
i18n (0.6.9)
|
|
88
|
-
listen (2.7.
|
|
85
|
+
listen (2.7.7)
|
|
89
86
|
celluloid (>= 0.15.2)
|
|
90
|
-
celluloid-io (>= 0.15.0)
|
|
91
87
|
rb-fsevent (>= 0.9.3)
|
|
92
88
|
rb-inotify (>= 0.9)
|
|
93
89
|
little-plugger (1.1.3)
|
|
@@ -95,39 +91,38 @@ GEM
|
|
|
95
91
|
logging (1.8.2)
|
|
96
92
|
little-plugger (>= 1.1.3)
|
|
97
93
|
multi_json (>= 1.8.4)
|
|
98
|
-
lumberjack (1.0.
|
|
94
|
+
lumberjack (1.0.6)
|
|
99
95
|
method_source (0.8.2)
|
|
100
|
-
mime-types (2.
|
|
101
|
-
mini_portile (0.
|
|
102
|
-
multi_json (1.10.
|
|
96
|
+
mime-types (2.3)
|
|
97
|
+
mini_portile (0.6.0)
|
|
98
|
+
multi_json (1.10.1)
|
|
103
99
|
net-scp (1.1.2)
|
|
104
100
|
net-ssh (>= 2.6.5)
|
|
105
|
-
net-ssh (2.9.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
mini_portile (~> 0.5.2)
|
|
101
|
+
net-ssh (2.9.1)
|
|
102
|
+
nokogiri (1.6.2.1)
|
|
103
|
+
mini_portile (= 0.6.0)
|
|
109
104
|
nori (1.1.5)
|
|
110
105
|
pry (0.9.12.6)
|
|
111
106
|
coderay (~> 1.0)
|
|
112
107
|
method_source (~> 0.8)
|
|
113
108
|
slop (~> 3.4)
|
|
114
109
|
rack (1.5.2)
|
|
115
|
-
rake (10.3.
|
|
110
|
+
rake (10.3.2)
|
|
116
111
|
rb-fsevent (0.9.4)
|
|
117
|
-
rb-inotify (0.9.
|
|
112
|
+
rb-inotify (0.9.5)
|
|
118
113
|
ffi (>= 0.5.0)
|
|
119
|
-
rb-kqueue (0.2.
|
|
114
|
+
rb-kqueue (0.2.3)
|
|
120
115
|
ffi (>= 0.5.0)
|
|
121
116
|
rest-client (1.6.7)
|
|
122
117
|
mime-types (>= 1.16)
|
|
123
|
-
rspec (2.99.0
|
|
124
|
-
rspec-core (
|
|
125
|
-
rspec-expectations (
|
|
126
|
-
rspec-mocks (
|
|
127
|
-
rspec-core (2.99.0
|
|
128
|
-
rspec-expectations (2.99.0
|
|
118
|
+
rspec (2.99.0)
|
|
119
|
+
rspec-core (~> 2.99.0)
|
|
120
|
+
rspec-expectations (~> 2.99.0)
|
|
121
|
+
rspec-mocks (~> 2.99.0)
|
|
122
|
+
rspec-core (2.99.0)
|
|
123
|
+
rspec-expectations (2.99.0)
|
|
129
124
|
diff-lcs (>= 1.1.3, < 2.0)
|
|
130
|
-
rspec-mocks (2.99.0
|
|
125
|
+
rspec-mocks (2.99.0)
|
|
131
126
|
rubyntlm (0.1.1)
|
|
132
127
|
savon (0.9.5)
|
|
133
128
|
akami (~> 1.0)
|
|
@@ -147,7 +142,7 @@ GEM
|
|
|
147
142
|
tins (~> 1.0)
|
|
148
143
|
thor (0.18.1)
|
|
149
144
|
timers (1.1.0)
|
|
150
|
-
tins (1.
|
|
145
|
+
tins (1.3.0)
|
|
151
146
|
uuidtools (2.1.4)
|
|
152
147
|
vagrant-omnibus (1.4.1)
|
|
153
148
|
wasabi (1.0.0)
|
|
@@ -171,7 +166,7 @@ DEPENDENCIES
|
|
|
171
166
|
guard-rspec
|
|
172
167
|
rake
|
|
173
168
|
rb-inotify
|
|
174
|
-
rspec (= 2.99.0
|
|
169
|
+
rspec (= 2.99.0)
|
|
175
170
|
vagrant!
|
|
176
171
|
vagrant-cachier!
|
|
177
172
|
vagrant-lxc!
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# vagrant-lxc
|
|
2
2
|
|
|
3
|
-
[](https://travis-ci.org/fgrehm/vagrant-lxc) [](http://badge.fury.io/rb/vagrant-lxc) [](https://codeclimate.com/github/fgrehm/vagrant-lxc) [](https://coveralls.io/r/fgrehm/vagrant-lxc) [](https://www.gittip.com/fgrehm/)
|
|
3
|
+
[](https://travis-ci.org/fgrehm/vagrant-lxc) [](http://badge.fury.io/rb/vagrant-lxc) [](https://codeclimate.com/github/fgrehm/vagrant-lxc) [](https://coveralls.io/r/fgrehm/vagrant-lxc) [](https://www.gittip.com/fgrehm/) [](https://gitter.im/fgrehm/vagrant-lxc)
|
|
4
4
|
|
|
5
5
|
[LXC](http://lxc.sourceforge.net/) provider for [Vagrant](http://www.vagrantup.com/) 1.1+
|
|
6
6
|
|
|
@@ -20,7 +20,7 @@ branch.
|
|
|
20
20
|
* Port forwarding via [`redir`](http://linux.die.net/man/1/redir)
|
|
21
21
|
|
|
22
22
|
As of now, it does not support public / private networks, but [private networks](https://github.com/fgrehm/vagrant-lxc/issues/120)
|
|
23
|
-
will be coming along
|
|
23
|
+
will be coming along with the final 1.0.0 release.
|
|
24
24
|
|
|
25
25
|
## Requirements
|
|
26
26
|
|
|
@@ -49,7 +49,7 @@ disable checksum offloading as described on [this comment](https://github.com/fg
|
|
|
49
49
|
On Vagrant 1.5+:
|
|
50
50
|
|
|
51
51
|
```
|
|
52
|
-
vagrant plugin install vagrant-lxc --plugin-version 1.0.0.alpha.
|
|
52
|
+
vagrant plugin install vagrant-lxc --plugin-version 1.0.0.alpha.3
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
On Vagrant < 1.5:
|
|
@@ -165,7 +165,7 @@ are not supported on mainstream kernels. To work around that, you can use the
|
|
|
165
165
|
whitelisting all commands required by `vagrant-lxc` to run.
|
|
166
166
|
|
|
167
167
|
If you are interested on what will be generated by that command, please check
|
|
168
|
-
[this code](lib/vagrant-lxc/
|
|
168
|
+
[this code](lib/vagrant-lxc/command/sudoers.rb).
|
|
169
169
|
|
|
170
170
|
_vagrant-lxc < 1.0.0 users, please check this [Wiki page](https://github.com/fgrehm/vagrant-lxc/wiki/Avoiding-%27sudo%27-passwords)_
|
|
171
171
|
|
data/lib/vagrant-lxc/action.rb
CHANGED
|
@@ -197,8 +197,14 @@ module Vagrant
|
|
|
197
197
|
def self.action_fetch_ip
|
|
198
198
|
Builder.new.tap do |b|
|
|
199
199
|
b.use Builtin::ConfigValidate
|
|
200
|
-
b.use
|
|
201
|
-
|
|
200
|
+
b.use Builtin::Call, Builtin::IsState, :running do |env, b2|
|
|
201
|
+
if env[:result]
|
|
202
|
+
b2.use FetchIpWithLxcAttach if env[:machine].provider.driver.supports_attach?
|
|
203
|
+
b2.use FetchIpFromDnsmasqLeases
|
|
204
|
+
else
|
|
205
|
+
b2.use Builtin::Message, I18n.t("vagrant_lxc.messages.not_running")
|
|
206
|
+
end
|
|
207
|
+
end
|
|
202
208
|
end
|
|
203
209
|
end
|
|
204
210
|
|
|
@@ -32,7 +32,7 @@ module Vagrant
|
|
|
32
32
|
|
|
33
33
|
# From: https://github.com/lxc/lxc/blob/staging/src/python-lxc/lxc/__init__.py#L371-L385
|
|
34
34
|
def get_container_ip_from_ip_addr(driver)
|
|
35
|
-
output = driver.attach '/sbin/ip', '-4', 'addr', 'show', 'scope', 'global', 'eth0', namespaces: 'network'
|
|
35
|
+
output = driver.attach '/sbin/ip', '-4', 'addr', 'show', 'scope', 'global', 'eth0', namespaces: ['network', 'mount']
|
|
36
36
|
if output =~ /^\s+inet ([0-9.]+)\/[0-9]+\s+/
|
|
37
37
|
return $1.to_s
|
|
38
38
|
end
|
|
@@ -40,135 +40,25 @@ module Vagrant
|
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
private
|
|
43
|
-
|
|
44
|
-
#
|
|
43
|
+
|
|
44
|
+
# This requires vagrant 1.5.2+ https://github.com/mitchellh/vagrant/commit/3371c3716278071680af9b526ba19235c79c64cb
|
|
45
45
|
def create_wrapper!
|
|
46
46
|
wrapper = Tempfile.new('lxc-wrapper').tap do |file|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
list[command] << args
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def list
|
|
57
|
-
@list ||= Hash.new do |key, hsh|
|
|
58
|
-
key[hsh] = []
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def allowed(command)
|
|
63
|
-
list[command] || []
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def run!(argv)
|
|
67
|
-
begin
|
|
68
|
-
command, args = `which \#{argv.shift}`.chomp, argv || []
|
|
69
|
-
check!(command, args)
|
|
70
|
-
puts `\#{command} \#{args.join(" ")}`
|
|
71
|
-
exit $?.to_i
|
|
72
|
-
rescue => e
|
|
73
|
-
STDERR.puts e.message
|
|
74
|
-
exit 1
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
def check!(command, args)
|
|
80
|
-
allowed(command).each do |checks|
|
|
81
|
-
return if valid_args?(args, checks)
|
|
82
|
-
end
|
|
83
|
-
raise_invalid(command, args)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def valid_args?(args, checks)
|
|
87
|
-
return false unless valid_length?(args, checks)
|
|
88
|
-
check = nil
|
|
89
|
-
args.each_with_index do |provided, i|
|
|
90
|
-
check = checks[i] unless check == '**'
|
|
91
|
-
return false unless match?(provided, check)
|
|
92
|
-
end
|
|
93
|
-
true
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
def valid_length?(args, checks)
|
|
97
|
-
args.length == checks.length || checks.last == '**'
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def match?(arg, check)
|
|
101
|
-
check == '**' || check.is_a?(Regexp) && !!check.match(arg) || arg == check
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def raise_invalid(command, args)
|
|
105
|
-
raise "Invalid arguments for command \#{command}, " <<
|
|
106
|
-
"provided args: \#{args.inspect}"
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
base = "/var/lib/lxc"
|
|
112
|
-
base_path = %r{\\A\#{base}/.*\\z}
|
|
113
|
-
templates_path = %r{\\A/usr/(share|lib|lib64|local/lib)/lxc/templates/.*\\z}
|
|
114
|
-
|
|
115
|
-
##
|
|
116
|
-
# Commands from provider.rb
|
|
117
|
-
# - Check lxc is installed
|
|
118
|
-
Whitelist.add '/usr/bin/which', /\\Alxc-\\w+\\z/
|
|
119
|
-
|
|
120
|
-
##
|
|
121
|
-
# Commands from driver.rb
|
|
122
|
-
# - Container config file
|
|
123
|
-
Whitelist.add '/bin/cat', base_path
|
|
124
|
-
# - Shared folders
|
|
125
|
-
Whitelist.add '/bin/mkdir', '-p', base_path
|
|
126
|
-
# - Container config customizations and pruning
|
|
127
|
-
Whitelist.add '/bin/cp', '-f', %r{/tmp/.*}, base_path
|
|
128
|
-
Whitelist.add '/bin/chown', 'root:root', base_path
|
|
129
|
-
# - Template import
|
|
130
|
-
Whitelist.add '/bin/cp', %r{\\A.*\\z}, templates_path
|
|
131
|
-
Whitelist.add '/bin/cp', %r{\\A.*\\z}, templates_path
|
|
132
|
-
Whitelist.add '/bin/cp', %r{\\A.*\\z}, templates_path
|
|
133
|
-
Whitelist.add '/bin/chmod', '+x', templates_path
|
|
134
|
-
# - Template removal
|
|
135
|
-
Whitelist.add '/bin/rm', templates_path
|
|
136
|
-
# - Packaging
|
|
137
|
-
Whitelist.add '/bin/tar', '--numeric-owner', '-cvzf', %r{/tmp/.*/rootfs.tar.gz}, '-C', base_path, './rootfs'
|
|
138
|
-
Whitelist.add '/bin/chown', /\\A\\d+:\\d+\\z/, %r{\\A/tmp/.*/rootfs\.tar\.gz\\z}
|
|
139
|
-
|
|
140
|
-
##
|
|
141
|
-
# Commands from driver/cli.rb
|
|
142
|
-
Whitelist.add '/usr/bin/lxc-version'
|
|
143
|
-
Whitelist.add '/usr/bin/lxc-ls'
|
|
144
|
-
Whitelist.add '/usr/bin/lxc-info', '--name', /.*/
|
|
145
|
-
Whitelist.add '/usr/bin/lxc-create', '-B', /.*/, '--template', /.*/, '--name', /.*/, '**'
|
|
146
|
-
Whitelist.add '/usr/bin/lxc-destroy', '--name', /.*/
|
|
147
|
-
Whitelist.add '/usr/bin/lxc-start', '-d', '--name', /.*/, '**'
|
|
148
|
-
Whitelist.add '/usr/bin/lxc-stop', '--name', /.*/
|
|
149
|
-
Whitelist.add '/usr/bin/lxc-shutdown', '--name', /.*/
|
|
150
|
-
Whitelist.add '/usr/bin/lxc-attach', '--name', /.*/, '**'
|
|
151
|
-
Whitelist.add '/usr/bin/lxc-attach', '-h'
|
|
152
|
-
|
|
153
|
-
##
|
|
154
|
-
# Commands from driver/action/remove_temporary_files.rb
|
|
155
|
-
Whitelist.add '/bin/rm', '-rf', %r{\\A\#{base}/.*/rootfs/tmp/.*}
|
|
156
|
-
|
|
157
|
-
# Watch out for stones
|
|
158
|
-
Whitelist.run!(ARGV)
|
|
159
|
-
EOF
|
|
47
|
+
template = Vagrant::Util::TemplateRenderer.new(
|
|
48
|
+
'sudoers.rb',
|
|
49
|
+
:template_root => Vagrant::LXC.source_root.join('templates').to_s,
|
|
50
|
+
:cmd_paths => build_cmd_paths_hash
|
|
51
|
+
)
|
|
52
|
+
file.puts template.render
|
|
160
53
|
end
|
|
161
54
|
wrapper.close
|
|
162
55
|
wrapper.path
|
|
163
56
|
end
|
|
164
57
|
|
|
165
|
-
# REFACTOR: Make use ERB rendering after https://github.com/mitchellh/vagrant/issues/3231
|
|
166
|
-
# lands into core
|
|
167
58
|
def create_sudoers!(user, command)
|
|
168
59
|
sudoers = Tempfile.new('vagrant-lxc-sudoers').tap do |file|
|
|
169
60
|
file.puts "# Automatically created by vagrant-lxc"
|
|
170
|
-
file.puts "
|
|
171
|
-
file.puts "#{user} ALL=(root) NOPASSWD: LXC"
|
|
61
|
+
file.puts "#{user} ALL=(root) NOPASSWD: #{command}"
|
|
172
62
|
end
|
|
173
63
|
sudoers.close
|
|
174
64
|
sudoers.path
|
|
@@ -185,6 +75,15 @@ Whitelist.run!(ARGV)
|
|
|
185
75
|
}.flatten
|
|
186
76
|
system "echo \"#{commands.join("; ")}\" | sudo sh"
|
|
187
77
|
end
|
|
78
|
+
|
|
79
|
+
def build_cmd_paths_hash
|
|
80
|
+
{}.tap do |hash|
|
|
81
|
+
%w( which cat mkdir cp chown chmod rm tar chown ).each do |cmd|
|
|
82
|
+
hash[cmd] = `which #{cmd}`.strip
|
|
83
|
+
end
|
|
84
|
+
hash['lxc_bin'] = Pathname(`which lxc-create`.strip).parent.to_s
|
|
85
|
+
end
|
|
86
|
+
end
|
|
188
87
|
end
|
|
189
88
|
end
|
|
190
89
|
end
|
data/lib/vagrant-lxc/driver.rb
CHANGED
|
@@ -84,6 +84,8 @@ module Vagrant
|
|
|
84
84
|
end
|
|
85
85
|
|
|
86
86
|
mount_options = Array(mount_options || ['bind'])
|
|
87
|
+
host_path = host_path.to_s.gsub(' ', '\\\040')
|
|
88
|
+
guest_path = guest_path.gsub(' ', '\\\040')
|
|
87
89
|
@customizations << ['mount.entry', "#{host_path} #{guest_path} none #{mount_options.join(',')} 0 0"]
|
|
88
90
|
end
|
|
89
91
|
|
|
@@ -102,10 +104,6 @@ module Vagrant
|
|
|
102
104
|
|
|
103
105
|
def forced_halt
|
|
104
106
|
@logger.info('Shutting down container...')
|
|
105
|
-
# TODO: Remove `lxc-shutdown` usage, graceful halt is enough
|
|
106
|
-
@cli.transition_to(:stopped) { |c| c.shutdown }
|
|
107
|
-
# REFACTOR: Do not use exception to control the flow
|
|
108
|
-
rescue CLI::TargetStateNotReached, CLI::ShutdownNotSupported
|
|
109
107
|
@cli.transition_to(:stopped) { |c| c.stop }
|
|
110
108
|
end
|
|
111
109
|
|
|
@@ -113,6 +111,10 @@ module Vagrant
|
|
|
113
111
|
@cli.destroy
|
|
114
112
|
end
|
|
115
113
|
|
|
114
|
+
def supports_attach?
|
|
115
|
+
@cli.supports_attach?
|
|
116
|
+
end
|
|
117
|
+
|
|
116
118
|
def attach(*command)
|
|
117
119
|
@cli.attach(*command)
|
|
118
120
|
end
|
|
@@ -10,7 +10,6 @@ module Vagrant
|
|
|
10
10
|
attr_accessor :name
|
|
11
11
|
|
|
12
12
|
class TransitionBlockNotProvided < RuntimeError; end
|
|
13
|
-
class ShutdownNotSupported < RuntimeError; end
|
|
14
13
|
class TargetStateNotReached < RuntimeError
|
|
15
14
|
def initialize(target_state, state)
|
|
16
15
|
msg = "Target state '#{target_state}' not reached, currently on '#{state}'"
|
|
@@ -77,19 +76,10 @@ module Vagrant
|
|
|
77
76
|
end
|
|
78
77
|
|
|
79
78
|
def stop
|
|
80
|
-
attach '/sbin/halt'
|
|
79
|
+
attach '/sbin/halt' if supports_attach?
|
|
81
80
|
run :stop, '--name', @name
|
|
82
81
|
end
|
|
83
82
|
|
|
84
|
-
def shutdown
|
|
85
|
-
if system('which lxc-shutdown > /dev/null')
|
|
86
|
-
run :shutdown, '--name', @name
|
|
87
|
-
else
|
|
88
|
-
# REFACTOR: Do not use exception to control the flow
|
|
89
|
-
raise ShutdownNotSupported
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
83
|
def attach(*cmd)
|
|
94
84
|
cmd = ['--'] + cmd
|
|
95
85
|
|
|
@@ -97,6 +87,11 @@ module Vagrant
|
|
|
97
87
|
opts = cmd.pop
|
|
98
88
|
namespaces = Array(opts[:namespaces]).map(&:upcase).join('|')
|
|
99
89
|
|
|
90
|
+
# HACK: The wrapper script should be able to handle this
|
|
91
|
+
if @sudo_wrapper.wrapper_path
|
|
92
|
+
namespaces = "'#{namespaces}'"
|
|
93
|
+
end
|
|
94
|
+
|
|
100
95
|
if namespaces
|
|
101
96
|
if supports_attach_with_namespaces?
|
|
102
97
|
extra = ['--namespaces', namespaces]
|
|
@@ -126,6 +121,19 @@ module Vagrant
|
|
|
126
121
|
end
|
|
127
122
|
end
|
|
128
123
|
|
|
124
|
+
def supports_attach?
|
|
125
|
+
unless defined?(@supports_attach)
|
|
126
|
+
begin
|
|
127
|
+
@supports_attach = true
|
|
128
|
+
run(:attach, '--name', @name, '--', '/bin/true')
|
|
129
|
+
rescue LXC::Errors::ExecuteError
|
|
130
|
+
@supports_attach = false
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
return @supports_attach
|
|
135
|
+
end
|
|
136
|
+
|
|
129
137
|
private
|
|
130
138
|
|
|
131
139
|
def run(command, *args)
|
|
@@ -4,6 +4,8 @@ module Vagrant
|
|
|
4
4
|
# Include this so we can use `Subprocess` more easily.
|
|
5
5
|
include Vagrant::Util::Retryable
|
|
6
6
|
|
|
7
|
+
attr_reader :wrapper_path
|
|
8
|
+
|
|
7
9
|
def initialize(wrapper_path = nil)
|
|
8
10
|
@wrapper_path = wrapper_path
|
|
9
11
|
@logger = Log4r::Logger.new("vagrant::lxc::sudo_wrapper")
|
data/lib/vagrant-lxc/version.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'vagrant-lxc/sudo_wrapper'
|
|
|
4
4
|
require 'vagrant-lxc/driver/cli'
|
|
5
5
|
|
|
6
6
|
describe Vagrant::LXC::Driver::CLI do
|
|
7
|
-
let(:sudo_wrapper) { double(Vagrant::LXC::SudoWrapper, run: true) }
|
|
7
|
+
let(:sudo_wrapper) { double(Vagrant::LXC::SudoWrapper, run: true, wrapper_path: nil) }
|
|
8
8
|
|
|
9
9
|
subject { described_class.new(sudo_wrapper) }
|
|
10
10
|
|
|
@@ -110,23 +110,42 @@ describe Vagrant::LXC::Driver::CLI do
|
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
describe '
|
|
113
|
+
describe 'stop' do
|
|
114
114
|
let(:name) { 'a-running-container' }
|
|
115
115
|
subject { described_class.new(sudo_wrapper, name) }
|
|
116
116
|
|
|
117
117
|
before do
|
|
118
|
-
subject.stub(system: true)
|
|
119
118
|
allow(subject).to receive(:run)
|
|
120
119
|
end
|
|
121
120
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
121
|
+
context 'lxc-attach is supported' do
|
|
122
|
+
before do
|
|
123
|
+
subject.stub(attach: true, supports_attach?: true)
|
|
124
|
+
subject.stop
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'runs a /sbin/halt within the container' do
|
|
128
|
+
expect(subject).to have_received(:attach).with('/sbin/halt')
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'issues a lxc-stop with provided container name' do
|
|
132
|
+
expect(subject).to have_received(:run).with(:stop, '--name', name)
|
|
133
|
+
end
|
|
125
134
|
end
|
|
126
135
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
context 'lxc-attach is not supported' do
|
|
137
|
+
before do
|
|
138
|
+
subject.stub(attach: false, supports_attach?: false)
|
|
139
|
+
subject.stop
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it 'runs a /sbin/halt within the container' do
|
|
143
|
+
expect(subject).to_not have_received(:attach)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
it 'issues a lxc-stop with provided container name' do
|
|
147
|
+
expect(subject).to have_received(:run).with(:stop, '--name', name)
|
|
148
|
+
end
|
|
130
149
|
end
|
|
131
150
|
end
|
|
132
151
|
|
|
@@ -183,9 +202,6 @@ describe Vagrant::LXC::Driver::CLI do
|
|
|
183
202
|
end
|
|
184
203
|
|
|
185
204
|
describe 'transition block' do
|
|
186
|
-
let(:name) { 'a-running-container' }
|
|
187
|
-
subject { described_class.new(name) }
|
|
188
|
-
|
|
189
205
|
before do
|
|
190
206
|
subject.stub(run: true, sleep: true, state: :stopped)
|
|
191
207
|
end
|
|
@@ -204,4 +220,33 @@ describe Vagrant::LXC::Driver::CLI do
|
|
|
204
220
|
|
|
205
221
|
skip 'waits for the expected container state'
|
|
206
222
|
end
|
|
223
|
+
|
|
224
|
+
describe 'check for whether lxc-attach is supported' do
|
|
225
|
+
let(:name) { 'a-running-container' }
|
|
226
|
+
subject { described_class.new(sudo_wrapper, name) }
|
|
227
|
+
|
|
228
|
+
context 'lxc-attach is present on system' do
|
|
229
|
+
before { subject.stub(run: true) }
|
|
230
|
+
|
|
231
|
+
it 'returns true if `lxc-attach --name CNAME -- /bin/true` works' do
|
|
232
|
+
expect(subject.supports_attach?).to be_truthy
|
|
233
|
+
expect(subject).to have_received(:run).with(
|
|
234
|
+
:attach, '--name', name, '--', '/bin/true'
|
|
235
|
+
)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
context 'lxc-attach is not present on system' do
|
|
240
|
+
before do
|
|
241
|
+
allow(subject).to receive(:run).and_raise(Vagrant::LXC::Errors::ExecuteError.new('msg'))
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'returns true if `lxc-attach --name CNAME -- /bin/true` works' do
|
|
245
|
+
expect(subject.supports_attach?).to be_falsy
|
|
246
|
+
expect(subject).to have_received(:run).with(
|
|
247
|
+
:attach, '--name', name, '--', '/bin/true'
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
207
252
|
end
|
data/spec/unit/driver_spec.rb
CHANGED
|
@@ -75,6 +75,17 @@ describe Vagrant::LXC::Driver do
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
describe 'supports_attach?' do
|
|
79
|
+
let(:cli) { double(Vagrant::LXC::Driver::CLI, supports_attach?: true) }
|
|
80
|
+
|
|
81
|
+
subject { described_class.new('name', nil, cli) }
|
|
82
|
+
|
|
83
|
+
it 'delegates to cli object' do
|
|
84
|
+
expect(subject.supports_attach?).to be_truthy
|
|
85
|
+
expect(cli).to have_received(:supports_attach?)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
78
89
|
describe 'start' do
|
|
79
90
|
let(:customizations) { [['a', '1'], ['b', '2']] }
|
|
80
91
|
let(:internal_customization) { ['internal', 'customization'] }
|
|
@@ -102,7 +113,7 @@ describe Vagrant::LXC::Driver do
|
|
|
102
113
|
end
|
|
103
114
|
|
|
104
115
|
describe 'halt' do
|
|
105
|
-
let(:cli) { double(Vagrant::LXC::Driver::CLI,
|
|
116
|
+
let(:cli) { double(Vagrant::LXC::Driver::CLI, stop: true) }
|
|
106
117
|
|
|
107
118
|
subject { described_class.new('name', nil, cli) }
|
|
108
119
|
|
|
@@ -110,8 +121,8 @@ describe Vagrant::LXC::Driver do
|
|
|
110
121
|
allow(cli).to receive(:transition_to).and_yield(cli)
|
|
111
122
|
end
|
|
112
123
|
|
|
113
|
-
it 'delegates to cli
|
|
114
|
-
expect(cli).to receive(:
|
|
124
|
+
it 'delegates to cli stop' do
|
|
125
|
+
expect(cli).to receive(:stop)
|
|
115
126
|
subject.forced_halt
|
|
116
127
|
end
|
|
117
128
|
|
|
@@ -122,14 +133,7 @@ describe Vagrant::LXC::Driver do
|
|
|
122
133
|
|
|
123
134
|
it 'attempts to force the container to stop in case a shutdown doesnt work' do
|
|
124
135
|
allow(cli).to receive(:shutdown).and_raise(Vagrant::LXC::Driver::CLI::TargetStateNotReached.new :target, :source)
|
|
125
|
-
expect(cli).to receive(:transition_to).with(:stopped)
|
|
126
|
-
expect(cli).to receive(:stop)
|
|
127
|
-
subject.forced_halt
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
it 'attempts to force the container to stop in case lxc-shutdown is not supported' do
|
|
131
|
-
allow(cli).to receive(:shutdown).and_raise(Vagrant::LXC::Driver::CLI::ShutdownNotSupported)
|
|
132
|
-
expect(cli).to receive(:transition_to).with(:stopped).twice
|
|
136
|
+
expect(cli).to receive(:transition_to).with(:stopped)
|
|
133
137
|
expect(cli).to receive(:stop)
|
|
134
138
|
subject.forced_halt
|
|
135
139
|
end
|
|
@@ -149,7 +153,8 @@ describe Vagrant::LXC::Driver do
|
|
|
149
153
|
describe 'folder sharing' do
|
|
150
154
|
let(:shared_folder) { {guestpath: '/vagrant', hostpath: '/path/to/host/dir'} }
|
|
151
155
|
let(:ro_rw_folder) { {guestpath: '/vagrant/ro_rw', hostpath: '/path/to/host/dir', mount_options: ['ro', 'rw']} }
|
|
152
|
-
let(:
|
|
156
|
+
let(:with_space_folder) { {guestpath: '/tmp/with space', hostpath: '/path/with space'} }
|
|
157
|
+
let(:folders) { [shared_folder, ro_rw_folder, with_space_folder] }
|
|
153
158
|
let(:rootfs_path) { Pathname('/path/to/rootfs') }
|
|
154
159
|
let(:expected_guest_path) { "vagrant" }
|
|
155
160
|
let(:sudo_wrapper) { double(Vagrant::LXC::SudoWrapper, run: true) }
|
|
@@ -178,5 +183,12 @@ describe Vagrant::LXC::Driver do
|
|
|
178
183
|
"#{ro_rw_folder[:hostpath]} vagrant/ro_rw none ro,rw 0 0"
|
|
179
184
|
]
|
|
180
185
|
end
|
|
186
|
+
|
|
187
|
+
it 'supports directories with spaces' do
|
|
188
|
+
expect(subject.customizations).to include [
|
|
189
|
+
'mount.entry',
|
|
190
|
+
"/path/with\\040space tmp/with\\040space none bind 0 0"
|
|
191
|
+
]
|
|
192
|
+
end
|
|
181
193
|
end
|
|
182
194
|
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/opt/vagrant/embedded/bin/ruby
|
|
2
|
+
# Automatically created by vagrant-lxc
|
|
3
|
+
|
|
4
|
+
class Whitelist
|
|
5
|
+
class << self
|
|
6
|
+
def add(command, *args)
|
|
7
|
+
list[command] << args
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def list
|
|
11
|
+
@list ||= Hash.new do |key, hsh|
|
|
12
|
+
key[hsh] = []
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def allowed(command)
|
|
17
|
+
list[command] || []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run!(argv)
|
|
21
|
+
begin
|
|
22
|
+
command, args = `which #{argv.shift}`.chomp, argv || []
|
|
23
|
+
check!(command, args)
|
|
24
|
+
puts `#{command} #{args.join(" ")}`
|
|
25
|
+
exit $?.to_i
|
|
26
|
+
rescue => e
|
|
27
|
+
STDERR.puts e.message
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
def check!(command, args)
|
|
34
|
+
allowed(command).each do |checks|
|
|
35
|
+
return if valid_args?(args, checks)
|
|
36
|
+
end
|
|
37
|
+
raise_invalid(command, args)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def valid_args?(args, checks)
|
|
41
|
+
return false unless valid_length?(args, checks)
|
|
42
|
+
check = nil
|
|
43
|
+
args.each_with_index do |provided, i|
|
|
44
|
+
check = checks[i] unless check == '**'
|
|
45
|
+
return false unless match?(provided, check)
|
|
46
|
+
end
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def valid_length?(args, checks)
|
|
51
|
+
args.length == checks.length || checks.last == '**'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def match?(arg, check)
|
|
55
|
+
check == '**' || check.is_a?(Regexp) && !!check.match(arg) || arg == check
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def raise_invalid(command, args)
|
|
59
|
+
raise "Invalid arguments for command #{command}, " <<
|
|
60
|
+
"provided args: #{args.inspect}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
base = "/var/lib/lxc"
|
|
66
|
+
base_path = %r{\A#{base}/.*\z}
|
|
67
|
+
templates_path = %r{\A/usr/(share|lib|lib64|local/lib)/lxc/templates/.*\z}
|
|
68
|
+
|
|
69
|
+
##
|
|
70
|
+
# Commands from provider.rb
|
|
71
|
+
# - Check lxc is installed
|
|
72
|
+
Whitelist.add '<%= cmd_paths['which'] %>', /\Alxc-\w+\z/
|
|
73
|
+
|
|
74
|
+
##
|
|
75
|
+
# Commands from driver.rb
|
|
76
|
+
# - Container config file
|
|
77
|
+
Whitelist.add '<%= cmd_paths['cat'] %>', base_path
|
|
78
|
+
# - Shared folders
|
|
79
|
+
Whitelist.add '<%= cmd_paths['mkdir'] %>', '-p', base_path
|
|
80
|
+
# - Container config customizations and pruning
|
|
81
|
+
Whitelist.add '<%= cmd_paths['cp'] %>', '-f', %r{/tmp/.*}, base_path
|
|
82
|
+
Whitelist.add '<%= cmd_paths['chown'] %>', 'root:root', base_path
|
|
83
|
+
# - Template import
|
|
84
|
+
Whitelist.add '<%= cmd_paths['cp'] %>', %r{\A.*\z}, templates_path
|
|
85
|
+
Whitelist.add '<%= cmd_paths['chmod'] %>', '+x', templates_path
|
|
86
|
+
# - Template removal
|
|
87
|
+
Whitelist.add '<%= cmd_paths['rm'] %>', templates_path
|
|
88
|
+
# - Packaging
|
|
89
|
+
Whitelist.add '<%= cmd_paths['tar'] %>', '--numeric-owner', '-cvzf', %r{/tmp/.*/rootfs.tar.gz}, '-C', base_path, './rootfs'
|
|
90
|
+
Whitelist.add '<%= cmd_paths['chown'] %>', /\A\d+:\d+\z/, %r{\A/tmp/.*/rootfs\.tar\.gz\z}
|
|
91
|
+
|
|
92
|
+
##
|
|
93
|
+
# Commands from driver/cli.rb
|
|
94
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-version'
|
|
95
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-ls'
|
|
96
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-info', '--name', /.*/
|
|
97
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-create', '-B', /.*/, '--template', /.*/, '--name', /.*/, '**'
|
|
98
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-destroy', '--name', /.*/
|
|
99
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-start', '-d', '--name', /.*/, '**'
|
|
100
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-stop', '--name', /.*/
|
|
101
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-shutdown', '--name', /.*/
|
|
102
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-attach', '--name', /.*/, '**'
|
|
103
|
+
Whitelist.add '<%= cmd_paths['lxc_bin'] %>/lxc-attach', '-h'
|
|
104
|
+
|
|
105
|
+
##
|
|
106
|
+
# Commands from driver/action/remove_temporary_files.rb
|
|
107
|
+
Whitelist.add '<%= cmd_paths['rm'] %>', '-rf', %r{\A#{base}/.*/rootfs/tmp/.*}
|
|
108
|
+
|
|
109
|
+
# Watch out for stones
|
|
110
|
+
Whitelist.run!(ARGV)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vagrant-lxc
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.0.0.alpha.
|
|
4
|
+
version: 1.0.0.alpha.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Fabio Rehm
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2014-
|
|
11
|
+
date: 2014-08-09 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Linux Containers provider for Vagrant
|
|
14
14
|
email:
|
|
@@ -85,13 +85,14 @@ files:
|
|
|
85
85
|
- spec/unit/support/unit_example_group.rb
|
|
86
86
|
- spec/unit_helper.rb
|
|
87
87
|
- tasks/spec.rake
|
|
88
|
+
- templates/sudoers.rb.erb
|
|
88
89
|
- vagrant-lxc.gemspec
|
|
89
90
|
- vagrant-spec.config.rb
|
|
90
91
|
homepage: https://github.com/fgrehm/vagrant-lxc
|
|
91
92
|
licenses:
|
|
92
93
|
- MIT
|
|
93
94
|
metadata: {}
|
|
94
|
-
post_install_message: "\n Thanks for giving vagrant-lxc 1.0.0.alpha.
|
|
95
|
+
post_install_message: "\n Thanks for giving vagrant-lxc 1.0.0.alpha.3 a try!\n This
|
|
95
96
|
version introduces many changes and includes some deprecations,\n please see the
|
|
96
97
|
project's CHANGELOG:\n https://github.com/fgrehm/vagrant-lxc/blob/master/CHANGELOG.md\n
|
|
97
98
|
\ "
|