chef-metal-docker 0.2 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,15 +6,18 @@ require 'uri'
6
6
  require 'socket'
7
7
  require 'em-proxy'
8
8
  require 'mixlib/shellout'
9
+ require 'sys/proctable'
10
+ require 'chef_metal_docker/chef_zero_http_proxy'
9
11
 
10
12
  module ChefMetalDocker
11
13
  class DockerTransport < ChefMetal::Transport
12
- def initialize(repository_name, container_name, credentials, connection)
13
- @repository_name = repository_name
14
+ def initialize(container_name, base_image_name, credentials, connection, tunnel_transport = nil)
15
+ @repository_name = 'chef'
14
16
  @container_name = container_name
15
- @image = Docker::Image.get("#{repository_name}:latest", connection)
17
+ @image = Docker::Image.get(base_image_name, connection)
16
18
  @credentials = credentials
17
19
  @connection = connection
20
+ @tunnel_transport = tunnel_transport
18
21
  end
19
22
 
20
23
  include Chef::Mixin::ShellOut
@@ -24,12 +27,16 @@ module ChefMetalDocker
24
27
  attr_reader :image
25
28
  attr_reader :credentials
26
29
  attr_reader :connection
30
+ attr_reader :tunnel_transport
27
31
 
28
32
  def execute(command, options={})
29
33
  Chef::Log.debug("execute '#{command}' with options #{options}")
34
+
30
35
  begin
31
36
  connection.post("/containers/#{container_name}/stop?t=0", '')
32
37
  Chef::Log.debug("stopped /containers/#{container_name}")
38
+ rescue Excon::Errors::NotModified
39
+ Chef::Log.debug("Already stopped #{container_name}")
33
40
  rescue Docker::Error::NotFoundError
34
41
  end
35
42
  begin
@@ -45,15 +52,17 @@ module ChefMetalDocker
45
52
  live_stream = nil
46
53
  live_stream = STDOUT if options[:stream]
47
54
  live_stream = options[:stream_stdout] if options[:stream_stdout]
48
- cmd = Mixlib::ShellOut.new(Shellwords.join(['docker', 'run', '--name', container_name, "#{repository_name}:latest" ] + command),
55
+ cmd = Mixlib::ShellOut.new(Shellwords.join(['docker', 'run', '--name', container_name, @image.id ] + command),
49
56
  :live_stream => live_stream, :timeout => execute_timeout(options))
57
+
58
+ Chef::Log.debug("Executing #{cmd}")
50
59
  cmd.run_command
51
60
 
52
61
 
53
62
  unless options[:read_only]
54
- Chef::Log.debug("Committing #{container_name} as #{repository_name}")
63
+ Chef::Log.debug("Committing #{container_name} as #{repository_name}:#{container_name}")
55
64
  container = Docker::Container.get(container_name)
56
- @image = container.commit('repo' => repository_name)
65
+ @image = container.commit('repo' => repository_name, 'tag' => container_name)
57
66
  end
58
67
 
59
68
  Chef::Log.debug("Execute complete: status #{cmd.exitstatus}")
@@ -62,7 +71,7 @@ module ChefMetalDocker
62
71
 
63
72
  def read_file(path)
64
73
  container = Docker::Container.create({
65
- 'Image' => "#{repository_name}:latest",
74
+ 'Image' => @image.id,
66
75
  'Cmd' => %w(echo true)
67
76
  }, connection)
68
77
  begin
@@ -99,7 +108,7 @@ module ChefMetalDocker
99
108
  Tempfile.open('metal_docker_write_file') do |file|
100
109
  file.write(content)
101
110
  file.close
102
- @image = @image.insert_local('localPath' => file.path, 'outputPath' => path, 't' => "#{repository_name}:latest")
111
+ @image = @image.insert_local('localPath' => file.path, 'outputPath' => path, 't' => "#{repository_name}:#{container_name}")
103
112
  end
104
113
  end
105
114
 
@@ -115,26 +124,38 @@ module ChefMetalDocker
115
124
  end
116
125
 
117
126
  def upload_file(local_path, path)
118
- @image = @image.insert_local('localPath' => local_path, 'outputPath' => path, 't' => "#{repository_name}:latest")
127
+ @image = @image.insert_local('localPath' => local_path, 'outputPath' => path, 't' => "#{repository_name}:#{container_name}")
119
128
  end
120
129
 
121
130
  def make_url_available_to_remote(url)
122
131
  # The host is already open to the container. Just find out its address and return it!
123
132
  uri = URI(url)
124
133
  host = Socket.getaddrinfo(uri.host, uri.scheme, nil, :STREAM)[0][3]
134
+ Chef::Log.debug("Making URL available: #{host}")
135
+
125
136
  if host == '127.0.0.1' || host == '[::1]'
126
137
  result = execute('ip route ls', :read_only => true)
138
+
139
+ Chef::Log.debug("IP route: #{result.stdout}")
140
+
127
141
  if result.stdout =~ /default via (\S+)/
128
- uri.host = $1
142
+
143
+ uri.host = if using_boot2docker?
144
+ # Intermediate VM does NAT, so local address should be fine here
145
+ Chef::Log.debug("Using boot2docker!")
146
+ IPSocket.getaddress(Socket.gethostname)
147
+ else
148
+ $1
149
+ end
129
150
 
130
151
  if !@proxy_thread
131
152
  # Listen to docker instances only, and forward to localhost
132
153
  @proxy_thread = Thread.new do
133
- Proxy.start(:host => uri.host, :port => uri.port, :debug => true) do |conn|
134
- conn.server :srv, :host => host, :port => uri.port
135
- end
154
+ Chef::Log.debug("Starting proxy thread: #{uri.host}:#{uri.port} <--> #{host}:#{uri.port}")
155
+ ChefZeroHttpProxy.new(uri.host, uri.port, host, uri.port).run
136
156
  end
137
157
  end
158
+ Chef::Log.debug("Using Chef server URL: #{uri.to_s}")
138
159
 
139
160
  return uri.to_s
140
161
  else
@@ -153,60 +174,15 @@ module ChefMetalDocker
153
174
 
154
175
  private
155
176
 
156
- def old_execute
157
- Chef::Log.debug("Creating #{container_name} from #{repository_name}:latest")
158
- @container = Docker::Container.create({
159
- 'name' => container_name,
160
- 'Image' => "#{repository_name}:latest",
161
- 'Cmd' => (command.is_a?(String) ? Shellwords.shellsplit(command) : command),
162
- 'AttachStdout' => true,
163
- 'AttachStderr' => true,
164
- 'TTY' => false
165
- }, connection)
166
-
167
- Docker.options[:read_timeout] = read_timeout
168
- begin
169
- stdout = ''
170
- stderr = ''
171
-
172
- Chef::Log.debug("Attaching to #{container_name}")
173
- # Capture stdout / stderr
174
- excon, attach_datum = attach_with_timeout(@container, read_timeout) do |type, str|
175
- puts "got something"
176
- case type
177
- when :stdout
178
- stdout << str
179
- stream_chunk(options, stdout, nil)
180
- when :stderr
181
- stderr << str
182
- stream_chunk(options, nil, stderr)
183
- else
184
- raise "unexpected message type #{type}"
177
+ # boot2docker introduces an intermediate VM so we need to use a slightly different
178
+ # mechanism for getting to the running chef-zero
179
+ def using_boot2docker?
180
+ Sys::ProcTable.ps do |proc|
181
+ if proc.respond_to?(:cmdline)
182
+ if proc.send(:cmdline).to_s =~ /.*--comment boot2docker.*/
183
+ return true
185
184
  end
186
185
  end
187
-
188
- begin
189
- Chef::Log.debug("Starting #{container_name}")
190
- # Start the container
191
- @container.start
192
-
193
- Chef::Log.debug("Grabbing exit status from #{container_name}")
194
- # Capture exit code
195
- exit_status = @container.wait(read_timeout)
196
-
197
- Chef::Log.debug("Waiting for attach to complete ...")
198
- wait_for_attach(excon, attach_datum)
199
-
200
- Chef::Log.debug("Execute complete: status #{exit_status['StatusCode']}")
201
- DockerResult.new(command, options, stdout, stderr, exit_status['StatusCode'])
202
- rescue
203
- # Make sure we close off outstanding connections if we exit the method
204
- excon.reset
205
- raise
206
- end
207
- ensure
208
- Chef::Log.debug("Removing temporary read timeout")
209
- Docker.options.delete(:read_timeout)
210
186
  end
211
187
  end
212
188
 
@@ -1,3 +1,3 @@
1
1
  module ChefMetalDocker
2
- VERSION = '0.2'
2
+ VERSION = '0.4.1'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chef-metal-docker
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.2'
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Duffield
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-14 00:00:00.000000000 Z
11
+ date: 2014-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: chef
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - '>='
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sys-proctable
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '>='
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '>='
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
84
  name: rspec
71
85
  requirement: !ruby/object:Gem::Requirement
@@ -102,21 +116,18 @@ extra_rdoc_files:
102
116
  - README.md
103
117
  - LICENSE
104
118
  files:
105
- - Rakefile
106
119
  - LICENSE
107
120
  - README.md
121
+ - Rakefile
108
122
  - lib/chef/provider/docker_container.rb
109
123
  - lib/chef/resource/docker_container.rb
110
- - lib/chef_metal/provisioner_init/docker_init.rb
111
- - lib/chef_metal_docker/docker_provisioner.rb
124
+ - lib/chef_metal/driver_init/docker.rb
125
+ - lib/chef_metal_docker.rb
126
+ - lib/chef_metal_docker/chef_zero_http_proxy.rb
127
+ - lib/chef_metal_docker/docker_container_machine.rb
128
+ - lib/chef_metal_docker/docker_driver.rb
112
129
  - lib/chef_metal_docker/docker_transport.rb
113
- - lib/chef_metal_docker/docker_unix_machine.rb
114
- - lib/chef_metal_docker/helpers/container/actions.rb
115
- - lib/chef_metal_docker/helpers/container/helpers.rb
116
- - lib/chef_metal_docker/helpers/container.rb
117
- - lib/chef_metal_docker/helpers.rb
118
130
  - lib/chef_metal_docker/version.rb
119
- - lib/chef_metal_docker.rb
120
131
  homepage: https://github.com/opscode/chef-metal-docker
121
132
  licenses: []
122
133
  metadata: {}
@@ -136,9 +147,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
136
147
  version: '0'
137
148
  requirements: []
138
149
  rubyforge_project:
139
- rubygems_version: 2.0.3
150
+ rubygems_version: 2.2.2
140
151
  signing_key:
141
152
  specification_version: 4
142
153
  summary: Provisioner for creating Docker containers in Chef Metal.
143
154
  test_files: []
144
- has_rdoc:
@@ -1,4 +0,0 @@
1
- require 'chef_metal_docker/docker_provisioner'
2
-
3
- ChefMetal.add_registered_provisioner_class("docker",
4
- ChefMetalDocker::DockerProvisioner)
@@ -1,272 +0,0 @@
1
- require 'chef_metal/provisioner'
2
- require 'chef_metal/convergence_strategy/no_converge'
3
- require 'chef_metal/convergence_strategy/install_cached'
4
- require 'chef_metal_docker/helpers/container'
5
- require 'chef_metal_docker/docker_transport'
6
- require 'chef_metal_docker/docker_unix_machine'
7
- require 'chef_metal/transport/ssh'
8
- require 'docker'
9
-
10
- module ChefMetalDocker
11
- class DockerProvisioner < ChefMetal::Provisioner
12
-
13
- include ChefMetalDocker::Helpers::Container
14
-
15
- def initialize(credentials = nil, connection = Docker.connection)
16
- @credentials = credentials
17
- @connection = connection
18
- end
19
-
20
- attr_reader :credentials
21
- attr_reader :connection
22
-
23
- # Inflate a provisioner from node information; we don't want to force the
24
- # driver to figure out what the provisioner really needs, since it varies
25
- # from provisioner to provisioner.
26
- #
27
- # ## Parameters
28
- # node - node to inflate the provisioner for
29
- #
30
- # returns a DockerProvisioner
31
- def self.inflate(node)
32
- self.new
33
- end
34
-
35
- #
36
- # Acquire a machine, generally by provisioning it. Returns a Machine
37
- # object pointing at the machine, allowing useful actions like setup,
38
- # converge, execute, file and directory. The Machine object will have a
39
- # "node" property which must be saved to the server (if it is any
40
- # different from the original node object).
41
- #
42
- # ## Parameters
43
- # action_handler - the action_handler object that plugs into the host.
44
- # node - node object (deserialized json) representing this machine. If
45
- # the node has a provisioner_options hash in it, these will be used
46
- # instead of options provided by the provisioner. TODO compare and
47
- # fail if different?
48
- # node will have node['normal']['provisioner_options'] in it with any options.
49
- # It is a hash with this format:
50
- #
51
- # -- provisioner_url: docker:<URL of Docker API endpoint>
52
- # -- base_image: Base image name to use, or repository_name:tag_name to use a specific tagged revision of that image
53
- # -- create_container: hash of a container to create. If present, no image will be created, just a container.
54
- # Hash options:
55
- # - command: command to run (if unspecified or nil, will spin up the container. If false, will not run anything and will just leave the image alone.)
56
- # - container_options: options for container create (see http://docs.docker.io/en/latest/reference/api/docker_remote_api_v1.10/#create-a-container)
57
- # - host_options: options for container start (see http://docs.docker.io/en/latest/reference/api/docker_remote_api_v1.10/#start-a-container)
58
- # - ssh_options: hash of ssh options. Presence of hash indicates sshd is running in the container. Net::SSH.new(ssh_options['username'], ssh_options) will be called. Set 'sudo' to true to sudo all commands (will be detected if username != root)
59
- #
60
- # node['normal']['provisioner_output'] will be populated with information
61
- # about the created machine. For lxc, it is a hash with this
62
- # format:
63
- #
64
- # -- provisioner_url: docker:<URL of Docker API endpoint>
65
- # -- container_name: docker container name
66
- # -- repository_name: docker image repository name from which container was inflated
67
- #
68
- def acquire_machine(action_handler, node)
69
- # Set up the modified node data
70
- provisioner_options = node['normal']['provisioner_options']
71
- provisioner_output = node['normal']['provisioner_output'] || {
72
- 'provisioner_url' => "docker:", # TODO put in the Docker API endpoint
73
- 'repository_name' => "#{node['name']}_image", # TODO disambiguate with chef_server_url/path!
74
- 'container_name' => node['name'] # TODO disambiguate with chef_server_url/path!
75
- }
76
-
77
- repository_name = provisioner_output['repository_name']
78
- container_name = provisioner_output['container_name']
79
- base_image_name = provisioner_options['base_image']
80
- raise "base_image not specified in provisioner options!" if !base_image_name
81
-
82
- if provisioner_options['create_container']
83
- create_container(action_handler, provisioner_options, provisioner_output)
84
- # We don't bother waiting ... our only job is to bring it up.
85
- else # We are in image build mode. Get prepped.
86
- # Tag the initial image. We aren't going to actually DO anything yet.
87
- # We will start up after we converge!
88
- base_image = Docker::Image.get(base_image_name)
89
- begin
90
- repository_image = Docker::Image.get("#{repository_name}:latest")
91
- # If the current image does NOT have the base_image as an ancestor,
92
- # we are going to have to re-tag it and rebuild.
93
- if repository_image.history.any? { |entry| entry['Id'] == base_image.id }
94
- tag_base_image = false
95
- else
96
- tag_base_image = true
97
- end
98
- rescue Docker::Error::NotFoundError
99
- tag_base_image = true
100
- end
101
- if tag_base_image
102
- action_handler.perform_action "Tag base image #{base_image_name} as #{repository_name}" do
103
- base_image.tag('repo' => repository_name, 'force' => true)
104
- end
105
- end
106
- end
107
-
108
- node['normal']['provisioner_output'] = provisioner_output
109
-
110
- if provisioner_options['create_container'] && provisioner_options['create_container']['ssh_options']
111
- action_handler.perform_action "wait for node to start ssh" do
112
- transport = transport_for(node)
113
- Timeout::timeout(5*60) do
114
- while !transport.available?
115
- sleep(0.5)
116
- end
117
- end
118
- end
119
- end
120
-
121
- # Nothing else needs to happen until converge. We already have the image we need!
122
- machine_for(node)
123
- end
124
-
125
- def connect_to_machine(node)
126
- machine_for(node)
127
- end
128
-
129
- def delete_machine(action_handler, node)
130
- if node['normal'] && node['normal']['provisioner_output']
131
- container_name = node['normal']['provisioner_output']['container_name']
132
- ChefMetal.inline_resource(action_handler) do
133
- docker_container container_name do
134
- action [:kill, :remove]
135
- end
136
- end
137
- end
138
- convergence_strategy_for(node).cleanup_convergence(action_handler, node)
139
- end
140
-
141
- def stop_machine(action_handler, node)
142
- if node['normal'] && node['normal']['provisioner_output']
143
- container_name = node['normal']['provisioner_output']['container_name']
144
- ChefMetal.inline_resource(action_handler) do
145
- docker_container container_name do
146
- action [:stop]
147
- end
148
- end
149
- end
150
- end
151
-
152
- # This is docker-only, not Metal, at the moment.
153
- # TODO this should be metal. Find a nice interface.
154
- def snapshot(action_handler, node, name=nil)
155
- container_name = node['normal']['provisioner_output']['container_name']
156
- ChefMetal.inline_resource(action_handler) do
157
- docker_container container_name do
158
- action [:commit]
159
- end
160
- end
161
- end
162
-
163
- # Output Docker tar format image
164
- # TODO this should be metal. Find a nice interface.
165
- def save_repository(action_handler, node, path)
166
- container_name = node['normal']['provisioner_output']['container_name']
167
- ChefMetal.inline_resource(action_handler) do
168
- docker_container container_name do
169
- action [:export]
170
- end
171
- end
172
- end
173
-
174
- # Load Docker tar format image into Docker repository
175
- def load_repository(path)
176
- end
177
-
178
- # Push an image back to Docker
179
- def push_image(name)
180
- end
181
-
182
- # Pull an image from Docker
183
- def pull_image(name)
184
- end
185
-
186
- private
187
-
188
- def machine_for(node)
189
- strategy = convergence_strategy_for(node)
190
- ChefMetalDocker::DockerUnixMachine.new(node, transport_for(node), convergence_strategy_for(node))
191
- end
192
-
193
- def convergence_strategy_for(node)
194
- provisioner_output = node['normal']['provisioner_output']
195
- provisioner_options = node['normal']['provisioner_options']
196
- strategy = begin
197
- options = {}
198
- provisioner_options = node['normal']['provisioner_options'] || {}
199
- options[:chef_client_timeout] = provisioner_options['chef_client_timeout'] if provisioner_options.has_key?('chef_client_timeout')
200
- ChefMetal::ConvergenceStrategy::InstallCached.new(options)
201
- end
202
- end
203
-
204
- def transport_for(node)
205
- provisioner_options = node['normal']['provisioner_options']
206
- provisioner_output = node['normal']['provisioner_output']
207
- if provisioner_options['create_container'] && provisioner_options['create_container']['ssh_options']
208
- container = Docker::Container.get(provisioner_output['container_name'])
209
- ssh_options = {
210
- # TODO create a user known hosts file
211
- # :user_known_hosts_file => vagrant_ssh_config['UserKnownHostsFile'],
212
- # :paranoid => true,
213
- :host_key_alias => "#{container.id}.docker"
214
- }.merge(provisioner_options['create_container']['ssh_options'])
215
- username = ssh_options.delete(:username)
216
- options = {}
217
- if ssh_options[:sudo] || (!ssh_options.has_key?(:sudo) && username != 'root')
218
- if ssh_options[:password]
219
- options[:prefix] = "echo #{ssh_options[:password]} | sudo -S -p '' "
220
- else
221
- options[:prefix] = 'sudo '
222
- end
223
- end
224
- ssh_options.delete(:sudo)
225
- ip_address = container.info['NetworkSettings']['IPAddress']
226
- Chef::Log.debug("Container #{provisioner_output['container_name']} address is #{ip_address}")
227
- ChefMetal::Transport::SSH.new(ip_address, username, ssh_options, options)
228
- else
229
- ChefMetalDocker::DockerTransport.new(
230
- provisioner_output['repository_name'],
231
- provisioner_output['container_name'],
232
- credentials,
233
- connection)
234
- end
235
- end
236
-
237
- def create_container(action_handler, provisioner_options, provisioner_output)
238
- container_name = provisioner_output['container_name']
239
-
240
- container_configuration = provisioner_options['create_container']['container_configuration'] || {}
241
- host_configuration = provisioner_options['create_container']['host_configuration'] || {}
242
- command = provisioner_options['create_container']['command']
243
- raise "Must pass create_container.command if creating a container" if !command
244
- command = command.split(/\s+/) if command.is_a?(String)
245
- container_configuration['Cmd'] = command
246
- need_to_create = false
247
- begin
248
- # Try to get the container; if that fails, it doesn't exist and we start it.
249
- container = Docker::Container.get(container_name)
250
- if !container.info['State']['Running']
251
- action_handler.perform_action "Delete old, non-running container" do
252
- container.delete
253
- end
254
- need_to_create = true
255
- end
256
-
257
- rescue Docker::Error::NotFoundError
258
- need_to_create = true
259
- end
260
-
261
- if need_to_create
262
- action_handler.perform_action "Create new container and run container_configuration['Cmd']" do
263
- container = Docker::Container.create({
264
- 'name' => container_name,
265
- 'Image' => provisioner_options['base_image']
266
- }.merge(container_configuration), connection)
267
- container.start!(host_configuration)
268
- end
269
- end
270
- end
271
- end
272
- end