miasma-lxd 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 768837675895e46e18d2454a8755dabf7a643689
4
+ data.tar.gz: 3696866fc4fae82a6c807826927acb6bd727dac2
5
+ SHA512:
6
+ metadata.gz: 9b76ccc003a7a69d5a83f1d4d0fc683ae344a7e4b9c80e121d81f05271306a7a62c89287a0dfa1cc3739380edcae548c627999a776fb9354d65799ff0cd7248e
7
+ data.tar.gz: 3c7c6f533d4c53855a5106118745356a9e77dfc060af697b18d1907d051b4fe83515b707622d9867d7753c2c6c61ca5dd9af5b4ca3a85f692ac3ffa9cd60d94f
data/CHANGELOG.md ADDED
@@ -0,0 +1,2 @@
1
+ # v0.1.0
2
+ * Initial release
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2015 Chris Roberts
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Miasma LXD
2
+
3
+ LXD API plugin for the miasma cloud library
4
+
5
+ ## Supported credential attributes:
6
+
7
+ Supported attributes used in the credentials section of API
8
+ configurations:
9
+
10
+ ```ruby
11
+ Miasma.api(
12
+ :type => :compute,
13
+ :provider => :lxd,
14
+ :credentials => {
15
+ ...
16
+ }
17
+ )
18
+ ```
19
+
20
+ ### Required attributes
21
+
22
+ * `api_endpoint` - LXD HTTPS endpoint (e.g. https://127.0.0.1:8443)
23
+ * `ssl_key` - Path to client SSL key
24
+ * `ssl_certificate` - Path to client SSL certificate
25
+
26
+ ### Initial connection required attributes
27
+
28
+ * `name` - Name of this client (defaults to hostname)
29
+ * `password` - Shared password with LXD to establish trust
30
+
31
+ ## Current support matrix
32
+
33
+ |Model |Create|Read|Update|Delete|
34
+ |--------------|------|----|------|------|
35
+ |AutoScale | | | | |
36
+ |BlockStorage | | | | |
37
+ |Compute | X | X | X | X |
38
+ |DNS | | | | |
39
+ |LoadBalancer | | | | |
40
+ |Network | | | | |
41
+ |Orchestration | | | | |
42
+ |Queues | | | | |
43
+ |Storage | | | | |
44
+
45
+ ## Info
46
+ * Repository: https://github.com/miasma-rb/miasma-lxd
@@ -0,0 +1,258 @@
1
+ require 'miasma'
2
+ require 'shellwords'
3
+ require 'bogo-websocket'
4
+
5
+ module Miasma
6
+ module Models
7
+ class Compute
8
+ # LXD compute API
9
+ class Lxd < Compute
10
+
11
+ include Contrib::Lxd::Api
12
+
13
+ # State mapping based on status
14
+ SERVER_STATE_MAP = Smash.new(
15
+ 'running' => :running,
16
+ 'stopped' => :stopped
17
+ )
18
+
19
+ # Reload a server model's data
20
+ #
21
+ # @param server [Miasma::Models::Compute::Server]
22
+ # @return [Miasma::Models::Compute::Server]
23
+ def server_reload(server)
24
+ result = request(
25
+ :path => "containers/#{server.id}",
26
+ :expects => [200, 404]
27
+ )
28
+ if(result[:response].code == 200)
29
+ result = result.get(:body, :metadata)
30
+ server.load_data(
31
+ :id => result[:name],
32
+ :name => result[:name],
33
+ :state => SERVER_STATE_MAP.fetch(result.get(:status, :status).downcase, :pending),
34
+ :status => result.fetch(:status, :status, 'unknown').downcase,
35
+ :addresses_private => (result.get(:status, :ips) || []).map{ |ip|
36
+ Server::Address.new(
37
+ :version => ip[:protocol].downcase.sub('ipv', '').to_i,
38
+ :address => ip[:address]
39
+ )
40
+ },
41
+ :image_id => 'unknown',
42
+ :flavor_id => result.get(:profiles).first,
43
+ :userdata => result.get(:userdata),
44
+ :custom => Smash.new(
45
+ :ephemeral => result[:ephemeral]
46
+ )
47
+ )
48
+ else
49
+ server.load_data(
50
+ :id => server.id,
51
+ :name => server.name,
52
+ :state => :terminated,
53
+ :status => 'terminated',
54
+ :addresses_private => [],
55
+ :image_id => 'none',
56
+ :flavor_id => 'none'
57
+ )
58
+ end
59
+ server.valid_state
60
+ end
61
+
62
+ # Destroy an existing server
63
+ #
64
+ # @param server [Miasma::Models::Compute::Server]
65
+ # @return [TrueClass, FalseClass]
66
+ def server_destroy(server)
67
+ if(server.persisted?)
68
+ if(server.state == :running)
69
+ result = request(
70
+ :path => "containers/#{server.id}/state",
71
+ :method => :put,
72
+ :expects => 202,
73
+ :json => {
74
+ :action => :stop,
75
+ :force => true
76
+ }
77
+ )
78
+ wait_for_operation(result.get(:body, :operation))
79
+ end
80
+ request(
81
+ :path => "containers/#{server.id}",
82
+ :method => :delete,
83
+ :expects => 202
84
+ )
85
+ true
86
+ else
87
+ false
88
+ end
89
+ end
90
+
91
+ # Save the server (create or update)
92
+ #
93
+ # @param server [Miasma::Models::Compute::Server]
94
+ # @return [Miasma::Models::Compute::Server]
95
+ def server_save(server)
96
+ if(server.persisted?)
97
+ else
98
+ result = request(
99
+ :path => 'containers',
100
+ :method => :post,
101
+ :expects => 202,
102
+ :json => {
103
+ :name => server.name,
104
+ :profiles => [server.flavor_id],
105
+ :ephemeral => server.custom.fetch(:ephemeral, false),
106
+ :source => {
107
+ :type => :image,
108
+ :alias => server.image_id
109
+ }
110
+ }
111
+ )
112
+ wait_for_operation(result.get(:body, :operation))
113
+ request(
114
+ :path => "containers/#{server.name}/state",
115
+ :method => :put,
116
+ :expects => 202,
117
+ :json => {
118
+ :action => :start
119
+ }
120
+ )
121
+ wait_for_operation(result.get(:body, :operation))
122
+ server.id = server.name
123
+ server
124
+ end
125
+ end
126
+
127
+ # Return all servers
128
+ #
129
+ # @return [Array<Miasma::Models::Compute::Server>]
130
+ def server_all
131
+ result = request(
132
+ :path => 'containers'
133
+ ).get(:body)
134
+ result.fetch(:metadata, []).map do |c_info|
135
+ c_name = c_info.sub("/#{version}/containers/", '')
136
+ Server.new(
137
+ self,
138
+ :id => c_name,
139
+ :name => c_name
140
+ ).valid_state
141
+ end
142
+ end
143
+
144
+ # Fetch file from server
145
+ #
146
+ # @param server [Miasma::Models::Compute::Server]
147
+ # @param path [String] remote path
148
+ # @return [IO-ish]
149
+ def server_get_file(server, path)
150
+ request(
151
+ :path => "containers/#{server.id}/files",
152
+ :params => {
153
+ :path => path
154
+ }
155
+ ).get(:body)
156
+ end
157
+
158
+ # Put file on server
159
+ #
160
+ # @param server [Miasma::Models::Compute::Server]
161
+ # @param io [IO-ish]
162
+ # @param remote_path [String]
163
+ # @return [TrueClass]
164
+ # @todo update to write in chunks when over `n` size
165
+ def server_put_file(server, io, remote_path, options={})
166
+ request(
167
+ :method => :post,
168
+ :path => "containers/#{server.id}/files",
169
+ :params => {
170
+ :path => remote_path
171
+ },
172
+ :body => io.read,
173
+ :headers => {
174
+ 'X-LXD-uid' => options.fetch(:uid, 0),
175
+ 'X-LXD-gid' => options.fetch(:gid, 0),
176
+ 'X-LXD-mode' => options.fetch(:mode, 0700)
177
+ }
178
+ )
179
+ true
180
+ end
181
+
182
+ # Execute command
183
+ #
184
+ # @param server [Miasma::Models::Compute::Server]
185
+ # @param command [String]
186
+ # @param options [Hash]
187
+ # @option options [IO] :stream write command output
188
+ # @return [TrueClass, FalseClass] command was successful
189
+ def server_execute(server, command, options={})
190
+ result = request(
191
+ :method => :post,
192
+ :path => "containers/#{server.id}/exec",
193
+ :expects => 202,
194
+ :json => Smash.new(
195
+ :command => Shellwords.shellwords(command),
196
+ :interactive => true,
197
+ 'wait-for-websocket' => true
198
+ )
199
+ )
200
+ dest = URI.parse(api_endpoint)
201
+ operation = result.get(:body, :operation).sub("/#{version}/operations/", '')
202
+ ws_common = Smash.new(
203
+ :ssl_key => ssl_key,
204
+ :ssl_certificate => ssl_certificate
205
+ )
206
+ if(options[:stream])
207
+ ws_common[:on_message] = proc{|message|
208
+ options[:stream].write(message)
209
+ }
210
+ end
211
+ websockets = Smash[
212
+ ['control', '0'].map do |fd_id|
213
+ fd_secret = result.get(:body, :metadata, :fds, fd_id)
214
+ [
215
+ fd_id,
216
+ Bogo::Websocket::Client.new(
217
+ ws_common.merge(
218
+ :destination => "wss://#{[dest.host, dest.port].compact.join(':')}",
219
+ :path => "/#{version}/operations/#{operation}/websocket",
220
+ :params => {
221
+ :secret => fd_secret
222
+ }
223
+ )
224
+ )
225
+ ]
226
+ end
227
+ ]
228
+ wait_for_operation(operation, options.fetch(:timeout, 20))
229
+ websockets.map(&:last).map(&:close)
230
+ result = request(
231
+ :path => "operations/#{operation}"
232
+ )
233
+ result.get(:body, :metadata, :metadata, :return) == 0
234
+ end
235
+
236
+ protected
237
+
238
+ # Wait for a remote operation to complete
239
+ #
240
+ # @param op_uuid [String]
241
+ # @param timeout [Integer]
242
+ # @return [TrueClass]
243
+ def wait_for_operation(op_uuid, timeout=20)
244
+ op_uuid = op_uuid.sub("/#{version}/operations/", '')
245
+ request(
246
+ :path => "operations/#{op_uuid}/wait",
247
+ :params => {
248
+ :status_code => 200,
249
+ :timeout => 20
250
+ }
251
+ )
252
+ true
253
+ end
254
+
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,103 @@
1
+ require 'miasma'
2
+
3
+ module Miasma
4
+ module Contrib
5
+ module Lxd
6
+ # API updates required for LXD communication
7
+ module Api
8
+
9
+ # Default API version to require
10
+ DEFAULT_API_VERSION = '1.0'
11
+
12
+ # Load required attributes into API class
13
+ #
14
+ # @param klass [Class]
15
+ def self.included(klass)
16
+ klass.class_eval do
17
+ attribute :name, String, :required => true, :default => Socket.gethostname
18
+ attribute :password, String
19
+ attribute :api_endpoint, String, :required => true
20
+ attribute :ssl_certificate, String, :required => true
21
+ attribute :ssl_key, String, :required => true
22
+ attribute :version, Gem::Version, :coerce => lambda{|v| Gem::Version.new(v.to_s)}, :required => true, :default => lambda{ Miasma::Contrib::Lxd::Api::DEFAULT_API_VERSION }
23
+ end
24
+ end
25
+
26
+ # @return [String] versioned endpoint
27
+ def endpoint
28
+ "#{api_endpoint}/#{version}"
29
+ end
30
+
31
+ # Clean up endpoint prior to data loading
32
+ #
33
+ # @param args [Hash]
34
+ def custom_setup(args)
35
+ if(args[:api_endpoint].to_s.end_with?('/'))
36
+ args[:api_endpoint] = args[:api_endpoint][0, endpoint.length - 1]
37
+ end
38
+ end
39
+
40
+ # Perform request
41
+ #
42
+ # @param connection [HTTP]
43
+ # @param http_method [Symbol]
44
+ # @param request_args [Array]
45
+ # @return [HTTP::Response]
46
+ def make_request(connection, http_method, request_args)
47
+ dest, options = request_args
48
+ options = Smash.new unless options
49
+ options[:ssl_context] = ssl_context
50
+ connection.send(http_method, dest, options)
51
+ end
52
+
53
+ protected
54
+
55
+ # @return [OpenSSL::SSL::SSLContext.new]
56
+ def ssl_context
57
+ memoize(:ssl_context) do
58
+ ctx = OpenSSL::SSL::SSLContext.new
59
+ ctx.cert = OpenSSL::X509::Certificate.new(File.read(ssl_certificate))
60
+ ctx.key = OpenSSL::PKey::RSA.new(File.read(ssl_key))
61
+ ctx
62
+ end
63
+ end
64
+
65
+ # Establish connection to endpoint and register client if not
66
+ # already registered
67
+ def connect
68
+ result = request(:endpoint => api_endpoint)[:body]
69
+ unless(result[:metadata].any?{|s| s.end_with?(version.to_s)})
70
+ raise InvalidVersionError
71
+ end
72
+ result = request(:endpoint => api_endpoint, :path => "/#{version}")[:body]
73
+ if(result[:auth] != 'trusted' && password)
74
+ authenticate_connection!
75
+ end
76
+ end
77
+
78
+ # Authenticate with the endpoint to create trusted connection
79
+ def authenticate_connection!
80
+ request(
81
+ :method => :post,
82
+ :path => 'certificates',
83
+ :json => {
84
+ :type => :client,
85
+ :name => name,
86
+ :password => password
87
+ }
88
+ )
89
+ end
90
+
91
+ # Always allow retry
92
+ #
93
+ # @return [TrueClass]
94
+ def retryable_allowed?(*_)
95
+ false
96
+ end
97
+
98
+ end
99
+ end
100
+ end
101
+
102
+ Models::Compute.autoload :Lxd, 'miasma/contrib/lxd/compute'
103
+ end
@@ -0,0 +1,4 @@
1
+ module MiasmaLxd
2
+ # Current library version
3
+ VERSION = Gem::Version.new('0.1.0')
4
+ end
data/lib/miasma-lxd.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'miasma'
2
+ require 'miasma-lxd/version'
@@ -0,0 +1,21 @@
1
+ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__)) + '/lib/'
2
+ require 'miasma-lxd/version'
3
+ Gem::Specification.new do |s|
4
+ s.name = 'miasma-lxd'
5
+ s.version = MiasmaLxd::VERSION.version
6
+ s.summary = 'Smoggy LXD API'
7
+ s.author = 'Chris Roberts'
8
+ s.email = 'code@chrisroberts.org'
9
+ s.homepage = 'https://github.com/miasma-rb/miasma-lxd'
10
+ s.description = 'Smoggy LXD API'
11
+ s.license = 'Apache 2.0'
12
+ s.require_path = 'lib'
13
+ s.add_runtime_dependency 'bogo-websocket', '< 1.0.0'
14
+ s.add_development_dependency 'miasma', '>= 0.2.31'
15
+ s.add_development_dependency 'pry'
16
+ s.add_development_dependency 'minitest'
17
+ s.add_development_dependency 'vcr'
18
+ s.add_development_dependency 'webmock'
19
+ s.add_development_dependency 'psych', '>= 2.0.8'
20
+ s.files = Dir['lib/**/*'] + %w(miasma-lxd.gemspec README.md CHANGELOG.md LICENSE)
21
+ end
metadata ADDED
@@ -0,0 +1,149 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: miasma-lxd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Chris Roberts
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-10-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bogo-websocket
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "<"
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "<"
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: miasma
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.2.31
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 0.2.31
41
+ - !ruby/object:Gem::Dependency
42
+ name: pry
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: vcr
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: psych
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 2.0.8
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: 2.0.8
111
+ description: Smoggy LXD API
112
+ email: code@chrisroberts.org
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - CHANGELOG.md
118
+ - LICENSE
119
+ - README.md
120
+ - lib/miasma-lxd.rb
121
+ - lib/miasma-lxd/version.rb
122
+ - lib/miasma/contrib/lxd.rb
123
+ - lib/miasma/contrib/lxd/compute.rb
124
+ - miasma-lxd.gemspec
125
+ homepage: https://github.com/miasma-rb/miasma-lxd
126
+ licenses:
127
+ - Apache 2.0
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubyforge_project:
145
+ rubygems_version: 2.4.8
146
+ signing_key:
147
+ specification_version: 4
148
+ summary: Smoggy LXD API
149
+ test_files: []