uricp 0.0.15 → 0.0.20
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/.rubocop.yml +3 -0
- data/Gemfile.lock +16 -21
- data/Jenkinsfile +90 -33
- data/Rakefile +24 -36
- data/almalinux8/Dockerfile +26 -0
- data/bin/uricp +25 -11
- data/bionic/Dockerfile +4 -3
- data/centos7/Dockerfile +17 -12
- data/cucumber.yml +1 -0
- data/features/check_uri_path.feature +14 -0
- data/features/rbd_access.feature +226 -0
- data/features/step_definitions/orbit_steps.rb +11 -4
- data/features/step_definitions/rbd_steps.rb +8 -0
- data/features/step_definitions/uricp_steps.rb +8 -4
- data/focal/Dockerfile +28 -0
- data/lib/segment_upload.rb +20 -22
- data/lib/uricp/curl_primitives.rb +31 -33
- data/lib/uricp/orbit_auth.rb +23 -27
- data/lib/uricp/segmenter.rb +12 -16
- data/lib/uricp/strategy/cache_common.rb +30 -12
- data/lib/uricp/strategy/cached_get.rb +19 -22
- data/lib/uricp/strategy/cleaner.rb +2 -8
- data/lib/uricp/strategy/common.rb +76 -18
- data/lib/uricp/strategy/local_convert.rb +10 -17
- data/lib/uricp/strategy/local_link.rb +10 -8
- data/lib/uricp/strategy/piped_cache.rb +11 -16
- data/lib/uricp/strategy/piped_cache_convert.rb +14 -18
- data/lib/uricp/strategy/piped_compress.rb +3 -8
- data/lib/uricp/strategy/piped_decompress.rb +7 -11
- data/lib/uricp/strategy/piped_local_compress.rb +0 -4
- data/lib/uricp/strategy/piped_local_decompress.rb +10 -16
- data/lib/uricp/strategy/piped_local_get.rb +0 -5
- data/lib/uricp/strategy/piped_local_put.rb +3 -9
- data/lib/uricp/strategy/piped_rbd_get.rb +30 -0
- data/lib/uricp/strategy/piped_remote_get.rb +20 -20
- data/lib/uricp/strategy/rbd_cache_base_snap.rb +27 -0
- data/lib/uricp/strategy/rbd_cache_check.rb +42 -0
- data/lib/uricp/strategy/rbd_cache_clone.rb +40 -0
- data/lib/uricp/strategy/rbd_cache_upload.rb +40 -0
- data/lib/uricp/strategy/rbd_cached_get.rb +36 -0
- data/lib/uricp/strategy/rbd_cached_put.rb +33 -0
- data/lib/uricp/strategy/rbd_flattener.rb +23 -0
- data/lib/uricp/strategy/rbd_put.rb +38 -0
- data/lib/uricp/strategy/rbd_snap.rb +56 -0
- data/lib/uricp/strategy/rbd_sweeper.rb +23 -0
- data/lib/uricp/strategy/remote_put.rb +11 -17
- data/lib/uricp/strategy/segmented_remote_put.rb +16 -28
- data/lib/uricp/strategy/sweeper.rb +2 -8
- data/lib/uricp/uri_strategy.rb +22 -13
- data/lib/uricp/version.rb +4 -2
- data/lib/uricp.rb +27 -19
- data/uricp.gemspec +3 -3
- data/xenial/Dockerfile +4 -3
- metadata +41 -25
- data/spec/something_spec.rb +0 -5
- data/trusty/Dockerfile +0 -20
@@ -1,45 +1,43 @@
|
|
1
1
|
module Uricp::CurlPrimitives
|
2
|
+
attr_reader :options
|
2
3
|
|
3
|
-
|
4
|
+
def from
|
5
|
+
options['from_uri']
|
6
|
+
end
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
+
def from=(target)
|
9
|
+
options['from_uri'] = target
|
10
|
+
end
|
8
11
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
+
def to
|
13
|
+
options['to_uri']
|
14
|
+
end
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
+
def to=(target)
|
17
|
+
options['to_uri'] = target
|
18
|
+
end
|
16
19
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def curl_command
|
22
|
-
"curl --fail --silent"
|
23
|
-
end
|
20
|
+
def curl_command
|
21
|
+
'curl --fail --silent'
|
22
|
+
end
|
24
23
|
|
25
|
-
|
26
|
-
|
27
|
-
|
24
|
+
def authentication
|
25
|
+
"-H X-Auth-Token:#{options['authenticator'].call}" if http_authentication?
|
26
|
+
end
|
28
27
|
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
def http_authentication?
|
29
|
+
options['authenticator']
|
30
|
+
end
|
32
31
|
|
33
|
-
|
34
|
-
|
35
|
-
|
32
|
+
def curl_upload_from(source, destination = to)
|
33
|
+
"#{curl_command} #{authentication} -T #{source} #{destination};"
|
34
|
+
end
|
36
35
|
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
def curl_download_to_pipe
|
37
|
+
"#{curl_command} #{authentication} #{from} |"
|
38
|
+
end
|
40
39
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
def curl_manifest(object_manifest, destination = to)
|
41
|
+
"#{curl_command} #{authentication} -X PUT -H 'X-Object-Manifest: #{object_manifest}' #{destination} --data-binary ''"
|
42
|
+
end
|
45
43
|
end
|
data/lib/uricp/orbit_auth.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
require 'open-uri'
|
2
2
|
module Uricp
|
3
|
-
|
4
3
|
class OrbitAuth
|
5
|
-
|
6
4
|
AuthenticationFailure = Class.new(ArgumentError)
|
7
5
|
|
8
6
|
def initialize(auth_uri, auth_id, auth_key)
|
@@ -14,9 +12,9 @@ module Uricp
|
|
14
12
|
def storage_url
|
15
13
|
@storage_url ||
|
16
14
|
begin
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
authenticate
|
16
|
+
@storage_url
|
17
|
+
end
|
20
18
|
end
|
21
19
|
|
22
20
|
def token
|
@@ -26,27 +24,27 @@ module Uricp
|
|
26
24
|
|
27
25
|
def self.validate_options(options)
|
28
26
|
if options['auth-token'] && (options['auth-key'] || options['auth-user'])
|
29
|
-
|
30
|
-
|
27
|
+
raise ::OptionParser::NeedlessArgument,
|
28
|
+
'use either key based or token based authentication'
|
31
29
|
end
|
32
30
|
if options['auth-key'].nil? ^ options['auth-user'].nil?
|
33
|
-
|
34
|
-
|
31
|
+
raise ::OptionParser::MissingArgument,
|
32
|
+
"'auth-user' requires 'auth-key'"
|
35
33
|
end
|
36
34
|
if (options['auth-token'] || options['auth-user']) && options['auth_uri'].nil?
|
37
|
-
|
38
|
-
|
35
|
+
raise ::OptionParser::NeedlessArgument,
|
36
|
+
'authentication is for http uris only'
|
39
37
|
end
|
40
38
|
end
|
41
39
|
|
42
40
|
def self.add_auth_token(options)
|
43
41
|
if options['auth-user']
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
orbit_credentials = new(options['auth_uri'],
|
43
|
+
options['auth-user'], options['auth-key'])
|
44
|
+
options['authenticator'] = orbit_credentials.method(:token)
|
47
45
|
elsif options['auth-token']
|
48
46
|
orbit_token = options['auth-token']
|
49
|
-
options['authenticator'] =
|
47
|
+
options['authenticator'] = -> { orbit_token }
|
50
48
|
end
|
51
49
|
options.delete('auth-key')
|
52
50
|
options.delete('auth-user')
|
@@ -54,29 +52,27 @@ module Uricp
|
|
54
52
|
end
|
55
53
|
|
56
54
|
def self.add_auth_to_optionparser(app)
|
57
|
-
app.on(
|
58
|
-
|
59
|
-
app.on(
|
60
|
-
|
61
|
-
app.on(
|
62
|
-
|
55
|
+
app.on('--auth-token AUTH_TOKEN',
|
56
|
+
'Use AUTH_TOKEN for non-local requests')
|
57
|
+
app.on('--auth-user AUTH_USER',
|
58
|
+
'Use AUTH_USER for authentication')
|
59
|
+
app.on('--auth-key AUTH_KEY',
|
60
|
+
'Use AUTH_KEY for authentication')
|
63
61
|
end
|
64
62
|
|
65
|
-
|
63
|
+
private
|
66
64
|
|
67
65
|
def authenticate
|
68
66
|
@auth_uri.open(
|
69
67
|
'X-Auth-User' => @auth_id,
|
70
|
-
|
71
|
-
|
68
|
+
'X-Auth-Key' => @auth_key,
|
69
|
+
'Range' => 'bytes=0-0'
|
72
70
|
) do |uri|
|
73
71
|
@storage_url = uri.meta['x-storage-url']
|
74
|
-
|
72
|
+
@token = uri.meta['x-auth-token']
|
75
73
|
end
|
76
74
|
rescue OpenURI::HTTPError => e
|
77
75
|
raise AuthenticationFailure, "Cannot authenticate against #{@auth_uri}"
|
78
76
|
end
|
79
|
-
|
80
77
|
end
|
81
|
-
|
82
78
|
end
|
data/lib/uricp/segmenter.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
require 'pathname'
|
2
2
|
|
3
3
|
module Uricp
|
4
|
-
|
5
4
|
class Segmenter
|
6
|
-
|
7
5
|
include Methadone::CLILogging
|
8
6
|
include Methadone::SH
|
9
7
|
include Uricp::CurlPrimitives
|
@@ -15,7 +13,7 @@ module Uricp
|
|
15
13
|
@stream = File.pipe?(source) || File.chardev?(source)
|
16
14
|
else
|
17
15
|
@source = STDIN
|
18
|
-
|
16
|
+
@stream = true
|
19
17
|
end
|
20
18
|
split_path
|
21
19
|
end
|
@@ -23,9 +21,9 @@ module Uricp
|
|
23
21
|
def split_path
|
24
22
|
elements = Pathname.new(to.path).enum_for(:each_filename).to_a
|
25
23
|
elements.shift
|
26
|
-
@account=elements.shift
|
27
|
-
@container=elements.shift
|
28
|
-
@object_path=File.join(elements)
|
24
|
+
@account = elements.shift
|
25
|
+
@container = elements.shift
|
26
|
+
@object_path = File.join(elements)
|
29
27
|
end
|
30
28
|
|
31
29
|
def upload
|
@@ -33,7 +31,7 @@ module Uricp
|
|
33
31
|
segmented_upload
|
34
32
|
else
|
35
33
|
sh! curl_upload_from(options['from']),
|
36
|
-
|
34
|
+
on_fail: "Upload to #{to} failed"
|
37
35
|
end
|
38
36
|
end
|
39
37
|
|
@@ -54,16 +52,16 @@ module Uricp
|
|
54
52
|
end
|
55
53
|
|
56
54
|
def object_manifest
|
57
|
-
@object_manifest ||= File.join(@container, @object_path, manifest_suffix)+'/'
|
55
|
+
@object_manifest ||= File.join(@container, @object_path, manifest_suffix) + '/'
|
58
56
|
end
|
59
57
|
|
60
58
|
def segmented_upload
|
61
59
|
debug "#{self.class.name}: Large upload detected - segmenting into #{segment_size} byte chunks."
|
62
60
|
suffix = 0
|
63
61
|
until @source.eof?
|
64
|
-
|
65
|
-
|
66
|
-
|
62
|
+
debug "#{self.class.name}: Uploading segment #{suffix}"
|
63
|
+
upload_segment(suffix)
|
64
|
+
suffix = suffix.next
|
67
65
|
end
|
68
66
|
add_manifest
|
69
67
|
end
|
@@ -71,18 +69,16 @@ module Uricp
|
|
71
69
|
def upload_segment(segment_number)
|
72
70
|
segment_name = File.join(to.to_s, manifest_suffix, '%08d' % segment_number)
|
73
71
|
debug "Uploading with #{curl_upload_from('-', segment_name)}"
|
74
|
-
open('|'+curl_upload_from('-', segment_name), 'w') do |destination|
|
72
|
+
open('|' + curl_upload_from('-', segment_name), 'w') do |destination|
|
75
73
|
copy_length = IO.copy_stream(@source, destination, segment_size)
|
76
|
-
|
74
|
+
debug "#{self.class.name}: Uploaded #{copy_length} bytes to #{segment_name}"
|
77
75
|
end
|
78
76
|
end
|
79
77
|
|
80
78
|
def add_manifest
|
81
79
|
debug "Adding DLO object_manifest #{object_manifest}"
|
82
80
|
sh! curl_manifest(object_manifest),
|
83
|
-
|
81
|
+
on_fail: "Upload to #{to} failed"
|
84
82
|
end
|
85
|
-
|
86
83
|
end
|
87
|
-
|
88
84
|
end
|
@@ -1,18 +1,40 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
|
3
3
|
module Uricp::Strategy
|
4
|
-
|
5
4
|
module CacheCommon
|
6
|
-
|
7
5
|
def validate_cache!
|
6
|
+
return if dry_run?
|
7
|
+
|
8
|
+
unless cache_exists?
|
9
|
+
raise Uricp::MissingCache,
|
10
|
+
"no cache found at #{cache_root}. Expected a 'cache' and 'temp' directory"
|
11
|
+
end
|
12
|
+
return if cache_name
|
13
|
+
|
8
14
|
raise Uricp::MissingCache,
|
9
|
-
|
10
|
-
|
11
|
-
|
15
|
+
'no cache name found'
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_active_cache
|
19
|
+
if cache_root
|
20
|
+
yield
|
21
|
+
else
|
22
|
+
debug "#{self.class.name}: no cacheing requested"
|
23
|
+
false
|
24
|
+
end
|
12
25
|
end
|
13
|
-
|
26
|
+
|
27
|
+
def without_active_cache
|
28
|
+
unless cache_root
|
29
|
+
yield
|
30
|
+
else
|
31
|
+
debug "#{self.class.name}: cache active - not appropriate"
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
14
36
|
def in_cache?
|
15
|
-
File.readable?
|
37
|
+
File.readable?(cache_file) || options['dry-cache']
|
16
38
|
end
|
17
39
|
|
18
40
|
def cache_root
|
@@ -24,7 +46,7 @@ module Uricp::Strategy
|
|
24
46
|
end
|
25
47
|
|
26
48
|
def cache_exists?
|
27
|
-
cache_root && %w
|
49
|
+
cache_root && %w[temp cache].all? do |d|
|
28
50
|
File.directory?(File.join(cache_root, d))
|
29
51
|
end
|
30
52
|
end
|
@@ -40,9 +62,5 @@ module Uricp::Strategy
|
|
40
62
|
def cache_file
|
41
63
|
@cache_file ||= File.join(cache_root, 'cache', cache_name)
|
42
64
|
end
|
43
|
-
|
44
65
|
end
|
45
|
-
|
46
66
|
end
|
47
|
-
|
48
|
-
|
@@ -1,48 +1,45 @@
|
|
1
1
|
require 'uri'
|
2
2
|
|
3
3
|
module Uricp::Strategy
|
4
|
-
|
5
4
|
class CachedGet
|
6
|
-
|
7
5
|
include Uricp::Strategy::Common
|
8
6
|
include Uricp::Strategy::CacheCommon
|
9
7
|
|
10
8
|
def appropriate?
|
11
|
-
|
9
|
+
with_active_cache do
|
12
10
|
validate_cache!
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
end
|
18
|
-
else
|
19
|
-
debug "#{self.class.name}: not appropriate"
|
11
|
+
return proposal if in_cache? || file_source?
|
12
|
+
|
13
|
+
debug "#{self.class.name}: no cache entry for #{options['from_uri']}"
|
14
|
+
false
|
20
15
|
end
|
21
|
-
false
|
22
16
|
end
|
23
17
|
|
24
18
|
def command
|
25
|
-
|
19
|
+
':;'
|
26
20
|
end
|
27
21
|
|
28
22
|
def proposal
|
29
23
|
@proposed_options = options.dup
|
30
|
-
unless file_source?
|
31
|
-
|
24
|
+
@proposed_options['from_uri'] = URI.join('file:///', cache_file) unless file_source?
|
25
|
+
if to.scheme == 'rbd'
|
26
|
+
@proposed_options['rbd_cache_name'] = rbd_cache_image_spec(to)
|
32
27
|
end
|
33
28
|
@proposed_options.delete('cache')
|
34
29
|
@proposed_options.delete('cache_name')
|
35
30
|
if conversion_required?
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
31
|
+
if dry_run?
|
32
|
+
@proposed_options['source-format'] = @proposed_options['target-format']
|
33
|
+
else
|
34
|
+
@proposed_options['source-format'] =
|
35
|
+
File.open(@proposed_options['from_uri'].path) { |f| encoding(f) }
|
36
|
+
end
|
37
|
+
if @proposed_options['source-format'] == @proposed_options['target-format']
|
38
|
+
@proposed_options.delete('source-format')
|
39
|
+
@proposed_options.delete('target-format')
|
40
|
+
end
|
42
41
|
end
|
43
42
|
self
|
44
43
|
end
|
45
|
-
|
46
44
|
end
|
47
|
-
|
48
45
|
end
|
@@ -1,13 +1,10 @@
|
|
1
1
|
module Uricp::Strategy
|
2
|
-
|
3
2
|
class Cleaner
|
4
|
-
|
5
3
|
include Uricp::Strategy::Common
|
6
4
|
|
7
5
|
def appropriate?
|
8
|
-
if options['clean'] && sequence_complete?
|
9
|
-
|
10
|
-
end
|
6
|
+
return proposal if options['clean'] && sequence_complete?
|
7
|
+
|
11
8
|
debug "#{self.class.name}: not appropriate"
|
12
9
|
false
|
13
10
|
end
|
@@ -21,8 +18,5 @@ module Uricp::Strategy
|
|
21
18
|
@proposed_options.delete('clean')
|
22
19
|
self
|
23
20
|
end
|
24
|
-
|
25
21
|
end
|
26
|
-
|
27
22
|
end
|
28
|
-
|
@@ -1,7 +1,5 @@
|
|
1
1
|
module Uricp::Strategy
|
2
|
-
|
3
2
|
module Common
|
4
|
-
|
5
3
|
include Methadone::CLILogging
|
6
4
|
include Uricp::CurlPrimitives
|
7
5
|
|
@@ -14,10 +12,10 @@ module Uricp::Strategy
|
|
14
12
|
|
15
13
|
def unsupported_transfer
|
16
14
|
raise Uricp::UnsupportedURLtype,
|
17
|
-
|
15
|
+
"Unsupported transfer from #{from} to #{to}"
|
18
16
|
end
|
19
17
|
|
20
|
-
alias
|
18
|
+
alias command unsupported_transfer
|
21
19
|
|
22
20
|
def all_local_files?
|
23
21
|
!sequence_complete? && file_source? && to.scheme == 'file'
|
@@ -44,24 +42,23 @@ module Uricp::Strategy
|
|
44
42
|
end
|
45
43
|
|
46
44
|
def qcow2?(magic)
|
47
|
-
magic.unpack('a3C') == ['QFI',0xfb]
|
45
|
+
magic.unpack('a3C') == ['QFI', 0xfb]
|
48
46
|
end
|
49
47
|
|
50
48
|
def encoding(io)
|
51
49
|
magic = io.read(4).to_s
|
52
|
-
|
53
|
-
when lz4?(magic)
|
50
|
+
if lz4?(magic)
|
54
51
|
:lz4
|
55
|
-
|
52
|
+
elsif qcow2?(magic)
|
56
53
|
version = io.read(4)
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
54
|
+
case version.unpack('N')
|
55
|
+
when [2]
|
56
|
+
:qcow2
|
57
|
+
when [3]
|
58
|
+
:qcow3
|
59
|
+
else
|
60
|
+
:qcow2un
|
61
|
+
end
|
65
62
|
else
|
66
63
|
:raw
|
67
64
|
end
|
@@ -73,6 +70,10 @@ module Uricp::Strategy
|
|
73
70
|
)
|
74
71
|
end
|
75
72
|
|
73
|
+
def dry_run?
|
74
|
+
options['dry-run']
|
75
|
+
end
|
76
|
+
|
76
77
|
def lz4_source?
|
77
78
|
options['source-format'] == :lz4
|
78
79
|
end
|
@@ -90,9 +91,16 @@ module Uricp::Strategy
|
|
90
91
|
end
|
91
92
|
|
92
93
|
PIPE_URI = URI('pipe:/')
|
94
|
+
def rbd_base_name
|
95
|
+
'base'.freeze
|
96
|
+
end
|
97
|
+
|
98
|
+
def rbd_snapshot_name
|
99
|
+
'uricp_snap'.freeze
|
100
|
+
end
|
93
101
|
|
94
102
|
def get_temp_filename(base_dir)
|
95
|
-
t = Time.now.strftime(
|
103
|
+
t = Time.now.strftime('%Y%m%d')
|
96
104
|
File.join(base_dir, "uricp-#{t}-#{$$}-#{rand(0x100000000).to_s(36)}")
|
97
105
|
end
|
98
106
|
|
@@ -108,6 +116,56 @@ module Uricp::Strategy
|
|
108
116
|
options['source-format'] && !lz4_source?
|
109
117
|
end
|
110
118
|
|
111
|
-
|
119
|
+
def rbd_image_spec(uri)
|
120
|
+
uri.path[1..-1]
|
121
|
+
end
|
122
|
+
|
123
|
+
def rbd_cache_image_spec(uri)
|
124
|
+
File.join(File.dirname(uri.path)[1..-1], options['cache_name'])
|
125
|
+
end
|
126
|
+
|
127
|
+
def rbd_cache_name
|
128
|
+
options['rbd_cache_name']
|
129
|
+
end
|
130
|
+
|
131
|
+
def rbd_clone_snapshot(cache=rbd_cache_name)
|
132
|
+
"#{cache}@#{rbd_base_name}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def rbd_uri(image_spec)
|
136
|
+
URI.join('rbd:///', image_spec)
|
137
|
+
end
|
112
138
|
|
139
|
+
def rbd_sequence_complete?
|
140
|
+
from.path.include?(to.path)
|
141
|
+
end
|
142
|
+
|
143
|
+
def rbd_id
|
144
|
+
'libvirt'
|
145
|
+
end
|
146
|
+
|
147
|
+
def in_rbd_cache?
|
148
|
+
options['in_rbd_cache']
|
149
|
+
end
|
150
|
+
|
151
|
+
def not_in_rbd_cache?
|
152
|
+
options['in_rbd_cache'] == false
|
153
|
+
end
|
154
|
+
|
155
|
+
def rbd_snapshot_spec?(uri)
|
156
|
+
uri.scheme == 'rbd' && uri.path.include?('@')
|
157
|
+
end
|
158
|
+
|
159
|
+
def in_rbd_cache(target)
|
160
|
+
result = false
|
161
|
+
if dry_run?
|
162
|
+
result = options['dry-cache'] && options['cache_name'] !~ /srv-...../
|
163
|
+
else
|
164
|
+
sh "rbd snap ls --id #{rbd_id} --format json #{target}" do |stdout|
|
165
|
+
result = JSON.parse(stdout).any? { |x| x['name'] == rbd_base_name }
|
166
|
+
end
|
167
|
+
end
|
168
|
+
result && rbd_clone_snapshot(target)
|
169
|
+
end
|
170
|
+
end
|
113
171
|
end
|