fog-proxmox 0.16.2 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4cdecf83035e83be1e7ceb19a56e551e65c0761ec0330ddfc23c834dccdf369c
4
- data.tar.gz: 7469eec6715cb9210eff2da78364d8b8d67a24fc7e4bb52dd809b5202135eb79
3
+ metadata.gz: 2a248dd1c4522b48cddfef2eccd5d07ee1c888fd72460ec333390e71b81803b5
4
+ data.tar.gz: 3ba917b2e4ae32138edabc4abc8cd65d5248eb6404f367bb11cd61a4c7395180
5
5
  SHA512:
6
- metadata.gz: 9588e62afafb53c22eac52bca2bc65d3e9c9adab8445fa2afa41c97b9933b0bcb49680f6a099df2c2f8ea8be1b5f270064e864991beb46cafdd7a949f3d5c28f
7
- data.tar.gz: e96d35aa10121df4325f45908276e65af1413d900e508b7d9064cae608f74dc0033886a80e139eac313114e93e6ff7a2215f2282739b660ce4a5ebff0a6c67ef
6
+ metadata.gz: 962ebb8637afce2db9c75156efcaf0d296c41b9027f881824bc096046016077126c2efd498caa346fa3837310d42a67ae0dfba7854124b9664c36b520f073044
7
+ data.tar.gz: a13abbba5806fd3414a52cfac3fb684c596eb7cd9edec744813274d41ca1afdf4d0e69e8d136ba9407691e60d24de4f2991f93aa2687186dadabdb4b13c6f726
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.0](https://github.com/fog/fog-proxmox/compare/v0.16.2...v1.0.0) (2026-09-14)
4
+
5
+
6
+ ### ⚠ BREAKING CHANGES
7
+
8
+ * Add support for ISO uploads via Proxmox API ([#148](https://github.com/fog/fog-proxmox/issues/148))
9
+
10
+ ### Features
11
+
12
+ * Add support for ISO uploads via Proxmox API ([#148](https://github.com/fog/fog-proxmox/issues/148)) ([396d08c](https://github.com/fog/fog-proxmox/commit/396d08c573feceda8443072d2c76cffd3c2a4918))
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * RuboCop extra spacing offenses ([#149](https://github.com/fog/fog-proxmox/issues/149)) ([9a303bc](https://github.com/fog/fog-proxmox/commit/9a303bc03a3d5049ded43d3ad1fa6ea79febd43e))
18
+
19
+
20
+ ### Miscellaneous Chores
21
+
22
+ * allow major releases before 1.0 ([#156](https://github.com/fog/fog-proxmox/issues/156)) ([0752d55](https://github.com/fog/fog-proxmox/commit/0752d55fcfa0628c452c3bc7d7a137e341c8da4a))
23
+
3
24
  ## [0.16.2](https://github.com/fog/fog-proxmox/compare/v0.16.1...v0.16.2) (2026-08-12)
4
25
 
5
26
 
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of Fog::Proxmox.
4
+
5
+ # Fog::Proxmox is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # Fog::Proxmox is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Fog::Proxmox. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ module Fog
19
+ module Proxmox
20
+ # An IO-like multipart body that streams a file between a prefix and suffix.
21
+ class MultipartBody
22
+ attr_reader :size
23
+
24
+ def initialize(prefix, file, suffix)
25
+ @prefix = prefix
26
+ @file = file
27
+ @suffix = suffix
28
+ @size = prefix.bytesize + file_size + suffix.bytesize
29
+ rewind
30
+ end
31
+
32
+ def read(length = nil, outbuf = nil)
33
+ length = @size - @position if length.nil?
34
+ raise ArgumentError, 'negative length' if length.negative?
35
+
36
+ result = ::String.new(encoding: Encoding::BINARY)
37
+ read_into(result, length)
38
+ result = nil if result.empty? && length.positive? && @part > 2
39
+
40
+ return result unless outbuf
41
+
42
+ outbuf.replace(result || '')
43
+ result && outbuf
44
+ end
45
+
46
+ def rewind
47
+ @file.rewind
48
+ @part = 0
49
+ @offset = 0
50
+ @position = 0
51
+ 0
52
+ end
53
+
54
+ def binmode
55
+ @file.binmode if @file.respond_to?(:binmode)
56
+ self
57
+ end
58
+
59
+ private
60
+
61
+ def file_size
62
+ return @file.size if @file.respond_to?(:size)
63
+ return @file.stat.size if @file.respond_to?(:stat)
64
+
65
+ raise ArgumentError, 'Upload file size cannot be determined'
66
+ end
67
+
68
+ def read_into(result, length)
69
+ while result.bytesize < length && @part <= 2
70
+ remaining = length - result.bytesize
71
+ chunk = @part == 1 ? @file.read(remaining) : read_string_part(remaining)
72
+
73
+ if chunk.nil? || chunk.empty?
74
+ @part += 1
75
+ @offset = 0
76
+ else
77
+ result << chunk
78
+ @position += chunk.bytesize
79
+ end
80
+ end
81
+ end
82
+
83
+ def read_string_part(length)
84
+ string = @part.zero? ? @prefix : @suffix
85
+ chunk = string.byteslice(@offset, length)
86
+ @offset += chunk.bytesize if chunk
87
+ chunk
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of Fog::Proxmox.
4
+
5
+ # Fog::Proxmox is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # Fog::Proxmox is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Fog::Proxmox. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'securerandom'
19
+ require 'excon'
20
+ require 'fog/proxmox/compute/models/multipart_body'
21
+
22
+ module Fog
23
+ module Proxmox
24
+ # Prepares a streamed multipart ISO upload and its content type.
25
+ class MultipartIsoUpload
26
+ attr_reader :body, :content_type
27
+
28
+ def initialize(body_params)
29
+ filename = body_params.fetch(:filename)
30
+ raise ArgumentError, "Invalid ISO filename: #{filename}" unless filename.match?(/\A[a-zA-Z0-9][a-zA-Z0-9._-]*\.iso\z/i)
31
+
32
+ file = body_params.fetch(:file)
33
+
34
+ boundary = '-' * 30 + SecureRandom.hex(15)
35
+ prefix = build_multipart_content(boundary, 'content', 'iso')
36
+ prefix << build_multipart_header(boundary, 'filename', filename: filename)
37
+ @body = MultipartBody.new(prefix, file, build_multipart_closing(boundary))
38
+
39
+ @content_type = "multipart/form-data; boundary=#{boundary}"
40
+ end
41
+
42
+ private
43
+
44
+ def build_multipart_header(boundary, name, filename: nil)
45
+ newline = Excon::CR_NL
46
+ header = ::String.new(encoding: Encoding::BINARY)
47
+ header << "--#{boundary}#{newline}"
48
+ header << %(Content-Disposition: form-data; name="#{name}")
49
+ header << %(; filename="#{filename}") if filename
50
+ header << newline
51
+ header << "Content-Type: application/octet-stream#{newline}" if filename
52
+ header << newline
53
+ end
54
+
55
+ def build_multipart_content(boundary, name, value)
56
+ build_multipart_header(boundary, name) << value << Excon::CR_NL
57
+ end
58
+
59
+ def build_multipart_closing(boundary)
60
+ "#{Excon::CR_NL}--#{boundary}--#{Excon::CR_NL}".b
61
+ end
62
+ end
63
+ end
64
+ end
@@ -26,6 +26,8 @@
26
26
 
27
27
  # frozen_string_literal: true
28
28
 
29
+ require 'fog/proxmox/storage/models/iso_upload'
30
+
29
31
  module Fog
30
32
  module Proxmox
31
33
  class Compute
@@ -53,6 +55,10 @@ module Fog
53
55
  super(new_attributes)
54
56
  end
55
57
 
58
+ def upload_iso(path)
59
+ Fog::Proxmox::Storage::IsoUpload.new(service: service, node_id: node_id, storage_id: identity, path: path).upload
60
+ end
61
+
56
62
  private
57
63
 
58
64
  def initialize_volumes
@@ -93,8 +93,8 @@ module Fog
93
93
  begin
94
94
  authenticate! if expired?
95
95
  request_options = params.merge(path: "#{@path}/#{params[:path]}",
96
- headers: @auth_token.headers(
97
- params[:method], params.respond_to?(:headers) ? params[:headers] : {}, {}
96
+ headers: (params[:headers] || {}).merge(
97
+ @auth_token.headers(params[:method], {}, {})
98
98
  ))
99
99
  response = @connection.request(request_options)
100
100
  rescue Excon::Errors::Unauthorized => e
@@ -102,6 +102,7 @@ module Fog
102
102
  if !%w[Bad username or password, invalid token
103
103
  value!].include?(e.response.body) && @proxmox_can_reauthenticate && !retried
104
104
  authenticate!
105
+ params[:body].rewind if params[:body].respond_to?(:rewind)
105
106
  retried = true
106
107
  retry
107
108
  # bad credentials or token renewal not possible
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fog
4
+ module Proxmox
5
+ class Storage
6
+ # Uploads a local ISO and waits for the Proxmox task to finish.
7
+ class IsoUpload < Fog::Model
8
+ attribute :node_id
9
+ attribute :storage_id
10
+ attribute :path
11
+
12
+ def upload
13
+ requires :node_id, :storage_id, :path
14
+
15
+ filename = File.basename(path)
16
+ size = File.size(path)
17
+ Fog::Logger.debug("Starting Proxmox ISO upload filename=#{filename} size=#{size} bytes node=#{node_id} storage=#{storage_id}")
18
+ response = File.open(path, 'rb') do |file|
19
+ Fog::Proxmox::Storage.new(service.config).upload_iso(
20
+ { node: node_id, storage: storage_id },
21
+ { filename: filename, file: file }
22
+ )
23
+ end
24
+ Fog::Logger.debug("Upload response: #{response.inspect}")
25
+ raise Fog::Errors::Error, "Unexpected upload response: #{response.inspect}" unless response.to_s.start_with?('UPID:')
26
+
27
+ service.nodes.get(node_id).tasks.wait_for(response)
28
+ response
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of Fog::Proxmox.
4
+
5
+ # Fog::Proxmox is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # Fog::Proxmox is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Fog::Proxmox. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'fog/proxmox/compute/models/multipart_iso_upload'
19
+
20
+ module Fog
21
+ module Proxmox
22
+ class Storage
23
+ # class Real upload_iso request
24
+ class Real
25
+ def upload_iso(path_params, body_params)
26
+ node = path_params[:node]
27
+ storage = path_params[:storage]
28
+ upload = MultipartIsoUpload.new(body_params)
29
+
30
+ request(
31
+ expects: [200],
32
+ method: 'POST',
33
+ path: "nodes/#{node}/storage/#{storage}/upload",
34
+ body: upload.body,
35
+ headers: { 'Content-Type' => upload.content_type }
36
+ )
37
+ end
38
+ end
39
+
40
+ # class Mock upload_iso request
41
+ class Mock
42
+ def upload_iso(_path_params, _body_params); end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -18,12 +18,70 @@
18
18
 
19
19
  # frozen_string_literal: true
20
20
 
21
+ require 'fog/proxmox/core'
22
+
21
23
  module Fog
22
24
  module Proxmox
23
- # Procmox storage service
25
+ # Proxmox storage service
24
26
  class Storage < Fog::Service
25
- # Models
26
- model_path 'fog/proxmox/storage'
27
+ requires :proxmox_url, :proxmox_auth_method
28
+ recognizes :proxmox_token, :proxmox_tokenid, :proxmox_userid, :persistent, :proxmox_username, :proxmox_password
29
+
30
+ model_path 'fog/proxmox/storage/models'
31
+ model :iso_upload
32
+
33
+ request_path 'fog/proxmox/storage/requests'
34
+ request :upload_iso
35
+
36
+ # Mock class
37
+ class Mock
38
+ attr_reader :config
39
+
40
+ def initialize(options = {})
41
+ @proxmox_uri = URI.parse(options[:proxmox_url])
42
+ @proxmox_auth_method = options[:proxmox_auth_method]
43
+ @proxmox_tokenid = options[:proxmox_tokenid]
44
+ @proxmox_userid = options[:proxmox_userid]
45
+ @proxmox_username = options[:proxmox_username]
46
+ @proxmox_password = options[:proxmox_password]
47
+ @proxmox_token = options[:proxmox_token]
48
+ @proxmox_path = @proxmox_uri.path
49
+ @config = options
50
+ end
51
+ end
52
+
53
+ # Real class
54
+ class Real
55
+ include Fog::Proxmox::Core
56
+
57
+ def initialize(options = {})
58
+ if options.respond_to?(:config_service?) && options.config_service?
59
+ configure(options)
60
+ else
61
+ super
62
+ end
63
+ end
64
+
65
+ def self.not_found_class
66
+ Fog::Proxmox::Storage::NotFound
67
+ end
68
+
69
+ def config
70
+ self
71
+ end
72
+
73
+ def config_service?
74
+ true
75
+ end
76
+
77
+ private
78
+
79
+ def configure(source)
80
+ source.instance_variables.each do |v|
81
+ instance_variable_set(v, source.instance_variable_get(v))
82
+ end
83
+ end
84
+ end
27
85
  end
28
86
  end
29
87
  end
@@ -19,6 +19,6 @@
19
19
 
20
20
  module Fog
21
21
  module Proxmox
22
- VERSION = '0.16.2'
22
+ VERSION = '1.0.0'
23
23
  end
24
24
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of Fog::Proxmox.
4
+
5
+ # Fog::Proxmox is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # Fog::Proxmox is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Fog::Proxmox. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'spec_helper'
19
+ require 'stringio'
20
+ require 'fog/proxmox/compute/models/multipart_body'
21
+
22
+ multipart_body_class = Fog::Proxmox::MultipartBody
23
+
24
+ # Match the implementation's compute/models location despite its shared namespace.
25
+ describe Fog::Proxmox::MultipartBody do # rubocop:disable RSpec/SpecFilePathFormat
26
+ it 'streams and rewinds all multipart sections' do
27
+ body = multipart_body_class.new('prefix', StringIO.new('file'), 'suffix')
28
+
29
+ _(body.size).must_equal 16
30
+ _(body.read(8)).must_equal 'prefixfi'
31
+ _(body.read).must_equal 'lesuffix'
32
+ _(body.read(1)).must_be_nil
33
+
34
+ body.rewind
35
+ _(body.read).must_equal 'prefixfilesuffix'
36
+ end
37
+
38
+ it 'clears the output buffer and returns nil at EOF' do
39
+ body = multipart_body_class.new('prefix', StringIO.new('file'), 'suffix')
40
+ body.read
41
+ outbuf = String.new('existing')
42
+
43
+ _(body.read(1, outbuf)).must_be_nil
44
+ _(outbuf).must_be_empty
45
+ end
46
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is part of Fog::Proxmox.
4
+
5
+ # Fog::Proxmox is free software: you can redistribute it and/or modify
6
+ # it under the terms of the GNU General Public License as published by
7
+ # the Free Software Foundation, either version 3 of the License, or
8
+ # (at your option) any later version.
9
+
10
+ # Fog::Proxmox is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with Fog::Proxmox. If not, see <http://www.gnu.org/licenses/>.
17
+
18
+ require 'spec_helper'
19
+ require 'stringio'
20
+ require 'fog/proxmox/storage/requests/upload_iso'
21
+
22
+ real_storage_class = Fog::Proxmox::Storage::Real
23
+
24
+ describe Fog::Proxmox::Storage::Real do
25
+ it 'loads the upload request and reuses an authenticated compute connection' do
26
+ auth_request = WebMock.stub_request(:get, 'https://pve.example.test/api2/json/access/users/root@pam/token/test')
27
+ .to_return(body: '{"data":{"expire":0}}')
28
+ compute = Fog::Proxmox::Compute.new(
29
+ proxmox_url: 'https://pve.example.test/api2/json',
30
+ proxmox_auth_method: 'user_token',
31
+ proxmox_userid: 'root@pam',
32
+ proxmox_tokenid: 'test',
33
+ proxmox_token: 'test-token'
34
+ )
35
+ storage = Fog::Proxmox::Storage.new(compute.config)
36
+
37
+ _(storage).must_be_kind_of real_storage_class
38
+ _(storage).must_respond_to :upload_iso
39
+ _(storage.instance_variable_get(:@connection)).must_be_same_as compute.instance_variable_get(:@connection)
40
+ _(storage.instance_variable_get(:@auth_token)).must_be_same_as compute.instance_variable_get(:@auth_token)
41
+ _(compute).wont_respond_to :upload_iso
42
+ ensure
43
+ WebMock.remove_request_stub(auth_request)
44
+ end
45
+
46
+ describe '#upload_iso' do
47
+ it 'streams a multipart ISO upload request' do
48
+ service = real_storage_class.allocate
49
+ iso_data = "cloud-init\x00\xFF".b
50
+ source = StringIO.new(iso_data)
51
+ read_lengths = []
52
+ file = Object.new
53
+ file.define_singleton_method(:size) { source.size }
54
+ file.define_singleton_method(:rewind) { source.rewind }
55
+ file.define_singleton_method(:read) do |length|
56
+ read_lengths << length
57
+ source.read(length)
58
+ end
59
+ source.read
60
+ request_options = nil
61
+
62
+ service.stub(:request, lambda { |options|
63
+ request_options = options
64
+ 'uploaded'
65
+ }) do
66
+ result = service.upload_iso(
67
+ { node: 'pve', storage: 'local' },
68
+ { filename: 'vm-cloudinit.iso', file: file }
69
+ )
70
+
71
+ _(result).must_equal 'uploaded'
72
+ end
73
+
74
+ _(request_options[:expects]).must_equal [200]
75
+ _(request_options[:method]).must_equal 'POST'
76
+ _(request_options[:path]).must_equal 'nodes/pve/storage/local/upload'
77
+ _(request_options[:headers]['Content-Type']).must_match(%r{\Amultipart/form-data; boundary=})
78
+ body = request_options[:body]
79
+ chunks = []
80
+ while (chunk = body.read(4))
81
+ chunks << chunk
82
+ end
83
+ encoded_body = chunks.join
84
+
85
+ _(body).must_be_kind_of Fog::Proxmox::MultipartBody
86
+ _(body.size).must_equal encoded_body.bytesize
87
+ _(encoded_body).must_include %(name="content"#{Excon::CR_NL}#{Excon::CR_NL}iso)
88
+ _(encoded_body).must_include %(name="filename"; filename="vm-cloudinit.iso")
89
+ _(encoded_body).must_include 'Content-Type: application/octet-stream'
90
+ _(encoded_body).must_include iso_data
91
+ _(read_lengths).wont_be_empty
92
+ _(read_lengths.all? { |length| length <= 4 }).must_equal true
93
+
94
+ body.rewind
95
+ _(body.read(body.size)).must_equal encoded_body
96
+ end
97
+
98
+ it 'rejects an unsafe filename' do
99
+ service = real_storage_class.allocate
100
+
101
+ error = _(proc do
102
+ service.upload_iso(
103
+ { node: 'pve', storage: 'local' },
104
+ { filename: "bad\r\nname.iso", file: StringIO.new('iso') }
105
+ )
106
+ end).must_raise ArgumentError
107
+
108
+ _(error.message).must_match(/Invalid ISO filename/)
109
+ end
110
+
111
+ it 'accepts an uppercase ISO extension' do
112
+ service = real_storage_class.allocate
113
+ service.stub(:request, 'uploaded') do
114
+ result = service.upload_iso(
115
+ { node: 'pve', storage: 'local' },
116
+ { filename: 'installer.ISO', file: StringIO.new('iso') }
117
+ )
118
+
119
+ _(result).must_equal 'uploaded'
120
+ end
121
+ end
122
+ end
123
+
124
+ describe '#request' do
125
+ it 'merges caller headers without overriding authentication headers' do
126
+ service = real_storage_class.allocate
127
+ auth_token = Object.new
128
+ auth_token.define_singleton_method(:headers) do |_method, _params, _additional_headers|
129
+ { 'Authorization' => 'PVEAPIToken=test' }
130
+ end
131
+
132
+ request_options = nil
133
+ response = Struct.new(:body).new('{"data":"uploaded"}')
134
+ connection = Object.new
135
+ connection.define_singleton_method(:request) do |options|
136
+ request_options = options
137
+ response
138
+ end
139
+
140
+ service.instance_variable_set(:@auth_token, auth_token)
141
+ service.instance_variable_set(:@connection, connection)
142
+ service.instance_variable_set(:@expires, nil)
143
+ service.instance_variable_set(:@path, '/api2/json')
144
+
145
+ result = service.send(
146
+ :request,
147
+ method: 'POST',
148
+ path: 'nodes/pve/storage/local/upload',
149
+ headers: {
150
+ 'Authorization' => 'caller-supplied-token',
151
+ 'Content-Type' => 'multipart/form-data; boundary=test'
152
+ }
153
+ )
154
+
155
+ _(result).must_equal 'uploaded'
156
+ _(request_options[:headers]).must_equal(
157
+ 'Authorization' => 'PVEAPIToken=test',
158
+ 'Content-Type' => 'multipart/form-data; boundary=test'
159
+ )
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'tempfile'
5
+ require 'fog/proxmox/attributes'
6
+ require 'fog/proxmox/compute/models/volumes'
7
+ require 'fog/proxmox/compute/models/storage'
8
+
9
+ storage_class = Fog::Proxmox::Compute::Storage
10
+
11
+ describe Fog::Proxmox::Compute::Storage do # rubocop:disable RSpec/SpecFilePathFormat
12
+ let(:service) { Minitest::Mock.new }
13
+ let(:upload_service) { Minitest::Mock.new }
14
+ let(:storage) { storage_class.new(service: service, node_id: 'pve', storage: 'local') }
15
+ let(:iso) { Tempfile.new(['cloudinit', '.iso']) }
16
+
17
+ attr_accessor :uploaded_file
18
+
19
+ before do
20
+ service.expect(:nil?, false)
21
+ storage
22
+ iso.binmode
23
+ iso.write("cloud-init\x00\xFF".b)
24
+ iso.flush
25
+ end
26
+
27
+ after do
28
+ iso.close!
29
+ end
30
+
31
+ def expect_upload(response, error: nil)
32
+ service.expect(:nil?, false)
33
+ service.expect(:config, :compute_config)
34
+ upload_service.expect(:upload_iso, response) do |path, body|
35
+ _(path).must_equal(node: 'pve', storage: 'local')
36
+ _(body[:filename]).must_equal File.basename(iso.path)
37
+ self.uploaded_file = body[:file]
38
+ _(uploaded_file.read).must_equal "cloud-init\x00\xFF".b
39
+ raise error if error
40
+
41
+ true
42
+ end
43
+ end
44
+
45
+ def upload_iso
46
+ factory = lambda do |config|
47
+ _(config).must_equal :compute_config
48
+ upload_service
49
+ end
50
+ Fog::Proxmox::Storage.stub(:new, factory) do
51
+ storage.upload_iso(iso.path)
52
+ end
53
+ ensure
54
+ upload_service.verify
55
+ end
56
+
57
+ def expect_wait(error: nil)
58
+ tasks = Object.new
59
+ test = self
60
+ tasks.define_singleton_method(:wait_for) do |upid|
61
+ test.assert_equal 'UPID:upload', upid
62
+ test.assert_predicate test.uploaded_file, :closed?
63
+ raise error if error
64
+
65
+ true
66
+ end
67
+ nodes = Minitest::Mock.new
68
+ nodes.expect(:get, Struct.new(:tasks).new(tasks), ['pve'])
69
+ service.expect(:nodes, nodes)
70
+ nodes
71
+ end
72
+
73
+ it 'uploads to its node and storage, closes the file, and waits before returning the UPID' do
74
+ expect_upload('UPID:upload')
75
+ nodes = expect_wait
76
+
77
+ _(upload_iso).must_equal 'UPID:upload'
78
+ service.verify
79
+ nodes.verify
80
+ end
81
+
82
+ it 'rejects an invalid response without polling tasks' do
83
+ expect_upload(nil)
84
+
85
+ error = _(proc { upload_iso }).must_raise Fog::Errors::Error
86
+ _(error.message).must_equal 'Unexpected upload response: nil'
87
+ _(uploaded_file.closed?).must_equal true
88
+ service.verify
89
+ end
90
+
91
+ it 'closes the file when the upload request fails' do
92
+ expect_upload(nil, error: Fog::Errors::Error.new('Upload failed'))
93
+
94
+ error = _(proc { upload_iso }).must_raise Fog::Errors::Error
95
+ _(error.message).must_equal 'Upload failed'
96
+ _(uploaded_file.closed?).must_equal true
97
+ end
98
+
99
+ it 'propagates task failures' do
100
+ expect_upload('UPID:upload')
101
+ nodes = expect_wait(error: Fog::Errors::Error.new('Task failed'))
102
+
103
+ error = _(proc { upload_iso }).must_raise Fog::Errors::Error
104
+ _(error.message).must_equal 'Task failed'
105
+ service.verify
106
+ nodes.verify
107
+ end
108
+ end
data/spec/proxmox_vcr.rb CHANGED
@@ -45,9 +45,9 @@ class ProxmoxVCR
45
45
 
46
46
  if use_recorded
47
47
  Fog.interval = 0
48
- @url = 'https://192.168.56.101:8006/api2/json'
48
+ @url = 'https://192.168.56.101:8006/api2/json'
49
49
  else
50
- @url = ENV['PROXMOX_URL']
50
+ @url = ENV['PROXMOX_URL']
51
51
  end
52
52
 
53
53
  VCR.configure do |config|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fog-proxmox
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tristan Robert
@@ -260,6 +260,8 @@ files:
260
260
  - lib/fog/proxmox/compute/models/efidisk.rb
261
261
  - lib/fog/proxmox/compute/models/interface.rb
262
262
  - lib/fog/proxmox/compute/models/interfaces.rb
263
+ - lib/fog/proxmox/compute/models/multipart_body.rb
264
+ - lib/fog/proxmox/compute/models/multipart_iso_upload.rb
263
265
  - lib/fog/proxmox/compute/models/node.rb
264
266
  - lib/fog/proxmox/compute/models/nodes.rb
265
267
  - lib/fog/proxmox/compute/models/server.rb
@@ -388,6 +390,8 @@ files:
388
390
  - lib/fog/proxmox/network/requests/power_node.rb
389
391
  - lib/fog/proxmox/network/requests/update_network.rb
390
392
  - lib/fog/proxmox/storage.rb
393
+ - lib/fog/proxmox/storage/models/iso_upload.rb
394
+ - lib/fog/proxmox/storage/requests/upload_iso.rb
391
395
  - lib/fog/proxmox/string.rb
392
396
  - lib/fog/proxmox/variables.rb
393
397
  - lib/fog/proxmox/version.rb
@@ -414,6 +418,9 @@ files:
414
418
  - spec/fixtures/proxmox/network/common_auth.yml
415
419
  - spec/fixtures/proxmox/network/networks.yml
416
420
  - spec/fixtures/proxmox/pve.home
421
+ - spec/fog/proxmox/compute/models/multipart_body_spec.rb
422
+ - spec/fog/proxmox/storage/real_upload_iso_spec.rb
423
+ - spec/fog/proxmox/storage/storage_upload_iso_spec.rb
417
424
  - spec/hash_spec.rb
418
425
  - spec/helpers/controller_helper_spec.rb
419
426
  - spec/helpers/cpu_helper_spec.rb