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 +4 -4
- data/CHANGELOG.md +21 -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 +8 -1
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,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
|
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
|
|
@@ -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
|