fog-proxmox 0.16.1 → 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 +4 -4
- data/CHANGELOG.md +28 -0
- data/lib/fog/proxmox/compute/models/multipart_body.rb +91 -0
- data/lib/fog/proxmox/compute/models/multipart_iso_upload.rb +64 -0
- data/lib/fog/proxmox/compute/models/storage.rb +6 -0
- data/lib/fog/proxmox/core.rb +3 -2
- data/lib/fog/proxmox/storage/models/iso_upload.rb +33 -0
- data/lib/fog/proxmox/storage/requests/upload_iso.rb +46 -0
- data/lib/fog/proxmox/storage.rb +61 -3
- data/lib/fog/proxmox/version.rb +1 -1
- data/spec/fog/proxmox/compute/models/multipart_body_spec.rb +46 -0
- data/spec/fog/proxmox/storage/real_upload_iso_spec.rb +162 -0
- data/spec/fog/proxmox/storage/storage_upload_iso_spec.rb +108 -0
- data/spec/proxmox_vcr.rb +2 -2
- metadata +9 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2a248dd1c4522b48cddfef2eccd5d07ee1c888fd72460ec333390e71b81803b5
|
|
4
|
+
data.tar.gz: 3ba917b2e4ae32138edabc4abc8cd65d5248eb6404f367bb11cd61a4c7395180
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 962ebb8637afce2db9c75156efcaf0d296c41b9027f881824bc096046016077126c2efd498caa346fa3837310d42a67ae0dfba7854124b9664c36b520f073044
|
|
7
|
+
data.tar.gz: a13abbba5806fd3414a52cfac3fb684c596eb7cd9edec744813274d41ca1afdf4d0e69e8d136ba9407691e60d24de4f2991f93aa2687186dadabdb4b13c6f726
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
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
|
+
|
|
24
|
+
## [0.16.2](https://github.com/fog/fog-proxmox/compare/v0.16.1...v0.16.2) (2026-08-12)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* Use Ruby version 3.0 instead of 3.1 in order to package in foreman-packaging. ([#146](https://github.com/fog/fog-proxmox/issues/146)) ([987ecc8](https://github.com/fog/fog-proxmox/commit/987ecc812402c1174a87dc086dd3e05842204617))
|
|
30
|
+
|
|
3
31
|
## [0.16.1](https://github.com/fog/fog-proxmox/compare/v0.16.0...v0.16.1) (2026-08-11)
|
|
4
32
|
|
|
5
33
|
|
|
@@ -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
|
data/lib/fog/proxmox/core.rb
CHANGED
|
@@ -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:
|
|
97
|
-
params[:method],
|
|
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
|
data/lib/fog/proxmox/storage.rb
CHANGED
|
@@ -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
|
-
#
|
|
25
|
+
# Proxmox storage service
|
|
24
26
|
class Storage < Fog::Service
|
|
25
|
-
|
|
26
|
-
|
|
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
|
data/lib/fog/proxmox/version.rb
CHANGED
|
@@ -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
|
|
48
|
+
@url = 'https://192.168.56.101:8006/api2/json'
|
|
49
49
|
else
|
|
50
|
-
@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.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tristan Robert
|
|
@@ -205,20 +205,6 @@ dependencies:
|
|
|
205
205
|
- - "~>"
|
|
206
206
|
- !ruby/object:Gem::Version
|
|
207
207
|
version: '3.5'
|
|
208
|
-
- !ruby/object:Gem::Dependency
|
|
209
|
-
name: excon
|
|
210
|
-
requirement: !ruby/object:Gem::Requirement
|
|
211
|
-
requirements:
|
|
212
|
-
- - ">="
|
|
213
|
-
- !ruby/object:Gem::Version
|
|
214
|
-
version: 1.5.0
|
|
215
|
-
type: :runtime
|
|
216
|
-
prerelease: false
|
|
217
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
218
|
-
requirements:
|
|
219
|
-
- - ">="
|
|
220
|
-
- !ruby/object:Gem::Version
|
|
221
|
-
version: 1.5.0
|
|
222
208
|
- !ruby/object:Gem::Dependency
|
|
223
209
|
name: fog-core
|
|
224
210
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -274,6 +260,8 @@ files:
|
|
|
274
260
|
- lib/fog/proxmox/compute/models/efidisk.rb
|
|
275
261
|
- lib/fog/proxmox/compute/models/interface.rb
|
|
276
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
|
|
277
265
|
- lib/fog/proxmox/compute/models/node.rb
|
|
278
266
|
- lib/fog/proxmox/compute/models/nodes.rb
|
|
279
267
|
- lib/fog/proxmox/compute/models/server.rb
|
|
@@ -402,6 +390,8 @@ files:
|
|
|
402
390
|
- lib/fog/proxmox/network/requests/power_node.rb
|
|
403
391
|
- lib/fog/proxmox/network/requests/update_network.rb
|
|
404
392
|
- lib/fog/proxmox/storage.rb
|
|
393
|
+
- lib/fog/proxmox/storage/models/iso_upload.rb
|
|
394
|
+
- lib/fog/proxmox/storage/requests/upload_iso.rb
|
|
405
395
|
- lib/fog/proxmox/string.rb
|
|
406
396
|
- lib/fog/proxmox/variables.rb
|
|
407
397
|
- lib/fog/proxmox/version.rb
|
|
@@ -428,6 +418,9 @@ files:
|
|
|
428
418
|
- spec/fixtures/proxmox/network/common_auth.yml
|
|
429
419
|
- spec/fixtures/proxmox/network/networks.yml
|
|
430
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
|
|
431
424
|
- spec/hash_spec.rb
|
|
432
425
|
- spec/helpers/controller_helper_spec.rb
|
|
433
426
|
- spec/helpers/cpu_helper_spec.rb
|
|
@@ -451,7 +444,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
451
444
|
requirements:
|
|
452
445
|
- - ">="
|
|
453
446
|
- !ruby/object:Gem::Version
|
|
454
|
-
version: '3.
|
|
447
|
+
version: '3.0'
|
|
455
448
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
456
449
|
requirements:
|
|
457
450
|
- - ">="
|