hammer_cli_import 0.10.21
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 +7 -0
- data/LICENSE +674 -0
- data/README.md +115 -0
- data/channel_data_pretty.json +10316 -0
- data/config/import/config_macros.yml +16 -0
- data/config/import/interview_answers.yml +13 -0
- data/config/import/role_map.yml +10 -0
- data/config/import.yml +2 -0
- data/lib/hammer_cli_import/activationkey.rb +156 -0
- data/lib/hammer_cli_import/all.rb +253 -0
- data/lib/hammer_cli_import/asynctasksreactor.rb +187 -0
- data/lib/hammer_cli_import/autoload.rb +27 -0
- data/lib/hammer_cli_import/base.rb +585 -0
- data/lib/hammer_cli_import/configfile.rb +392 -0
- data/lib/hammer_cli_import/contenthost.rb +243 -0
- data/lib/hammer_cli_import/contentview.rb +198 -0
- data/lib/hammer_cli_import/csvhelper.rb +68 -0
- data/lib/hammer_cli_import/deltahash.rb +86 -0
- data/lib/hammer_cli_import/fixtime.rb +27 -0
- data/lib/hammer_cli_import/hostcollection.rb +52 -0
- data/lib/hammer_cli_import/import.rb +31 -0
- data/lib/hammer_cli_import/importtools.rb +351 -0
- data/lib/hammer_cli_import/organization.rb +110 -0
- data/lib/hammer_cli_import/persistentmap.rb +225 -0
- data/lib/hammer_cli_import/repository.rb +91 -0
- data/lib/hammer_cli_import/repositoryenable.rb +250 -0
- data/lib/hammer_cli_import/templatesnippet.rb +67 -0
- data/lib/hammer_cli_import/user.rb +155 -0
- data/lib/hammer_cli_import/version.rb +25 -0
- data/lib/hammer_cli_import.rb +53 -0
- metadata +117 -0
@@ -0,0 +1,198 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 Red Hat Inc.
|
3
|
+
#
|
4
|
+
# This file is part of hammer-cli-import.
|
5
|
+
#
|
6
|
+
# hammer-cli-import is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# hammer-cli-import is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with hammer-cli-import. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'set'
|
21
|
+
|
22
|
+
module HammerCLIImport
|
23
|
+
class ImportCommand
|
24
|
+
class LocalRepositoryImportCommand < BaseCommand
|
25
|
+
extend ImportTools::Repository::Extend
|
26
|
+
include ImportTools::Repository::Include
|
27
|
+
include ImportTools::ContentView::Include
|
28
|
+
|
29
|
+
command_name 'content-view'
|
30
|
+
desc 'Create Content Views based on local/cloned Channels (from spacewalk-export-channels).'
|
31
|
+
|
32
|
+
csv_columns 'org_id', 'channel_id', 'channel_label', 'channel_name'
|
33
|
+
|
34
|
+
persistent_maps :organizations, :repositories, :local_repositories, :content_views,
|
35
|
+
:products, :redhat_repositories, :redhat_content_views, :system_content_views
|
36
|
+
|
37
|
+
option ['--dir'], 'DIR', 'Export directory'
|
38
|
+
option ['--filter'], :flag, 'Filter content-views for package names present in Sat5 channel', :default => false
|
39
|
+
add_repo_options
|
40
|
+
|
41
|
+
def directory
|
42
|
+
File.expand_path(option_dir || File.dirname(option_csv_file))
|
43
|
+
end
|
44
|
+
|
45
|
+
def mk_product_hash(data, product_name)
|
46
|
+
{
|
47
|
+
:name => product_name,
|
48
|
+
:organization_id => get_translated_id(:organizations, data['org_id'].to_i)
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def mk_repo_hash(data, product_id)
|
53
|
+
{
|
54
|
+
:name => "Local repository for #{data['channel_label']}",
|
55
|
+
:product_id => product_id,
|
56
|
+
:url => 'file://' + File.join(directory, data['org_id'], data['channel_id']),
|
57
|
+
:content_type => 'yum'
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def mk_content_view_hash(data, repo_ids)
|
62
|
+
{
|
63
|
+
:name => data['channel_name'],
|
64
|
+
|
65
|
+
:description => 'Channel migrated from Satellite 5',
|
66
|
+
|
67
|
+
:organization_id => get_translated_id(:organizations, data['org_id'].to_i),
|
68
|
+
:repository_ids => repo_ids
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
def newer_repositories(cw)
|
73
|
+
last = cw['last_published']
|
74
|
+
return true unless last
|
75
|
+
last = Time.parse(last)
|
76
|
+
cw['repositories'].any? do |repo|
|
77
|
+
repo['last_sync'].nil? || last < Time.parse(repo['last_sync'])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def push_unless_nil(col, obj)
|
82
|
+
col << obj unless obj.nil?
|
83
|
+
end
|
84
|
+
|
85
|
+
def load_custom_channel_info(org_id, channel_id)
|
86
|
+
headers = %w(org_id channel_id package_nevra package_rpm_name in_repo in_parent_channel)
|
87
|
+
file = File.join directory, org_id.to_s, channel_id.to_s + '.csv'
|
88
|
+
|
89
|
+
packages_in_channel = Set[]
|
90
|
+
repo_ids = Set[]
|
91
|
+
parent_channel_ids = Set[]
|
92
|
+
has_local_packages = false
|
93
|
+
|
94
|
+
CSVHelper.csv_each file, headers do |data|
|
95
|
+
packages_in_channel << data['package_nevra']
|
96
|
+
push_unless_nil parent_channel_ids, data['in_parent_channel']
|
97
|
+
push_unless_nil repo_ids, data['in_repo']
|
98
|
+
has_local_packages ||= data['in_repo'].nil? && data['in_parent_channel'].nil?
|
99
|
+
end
|
100
|
+
|
101
|
+
raise "Multiple parents for channel #{channel_id}?" unless parent_channel_ids.size.between? 0, 1
|
102
|
+
|
103
|
+
[repo_ids.to_a, parent_channel_ids.to_a, packages_in_channel.to_a, has_local_packages]
|
104
|
+
end
|
105
|
+
|
106
|
+
def add_local_repo(data)
|
107
|
+
product_name = 'Local-repositories'
|
108
|
+
composite_id = [data['org_id'].to_i, product_name]
|
109
|
+
product_hash = mk_product_hash data, product_name
|
110
|
+
product_id = create_entity(:products, product_hash, composite_id)['id'].to_i
|
111
|
+
|
112
|
+
repo_hash = mk_repo_hash data, product_id
|
113
|
+
local_repo = create_entity :local_repositories, repo_hash, [data['org_id'].to_i, data['channel_id'].to_i]
|
114
|
+
local_repo
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_repo_filters(content_view_id, nevras)
|
118
|
+
cw_filter = api_call :content_view_filters,
|
119
|
+
:create,
|
120
|
+
{ :content_view_id => content_view_id,
|
121
|
+
:name => 'Satellite 5 channel equivalence filter',
|
122
|
+
:type => 'rpm',
|
123
|
+
:inclusion => true}
|
124
|
+
|
125
|
+
packages = nevras.collect do |package_nevra|
|
126
|
+
match = /^([^:]+)-(\d+):([^-]+)-(.*)\.([^.]*)$/.match(package_nevra)
|
127
|
+
raise "Bad nevra: #{package_nevra}" unless match
|
128
|
+
|
129
|
+
{ :name => match[1],
|
130
|
+
:epoch => match[2],
|
131
|
+
:version => match[3],
|
132
|
+
:release => match[4],
|
133
|
+
:architecture => match[5]
|
134
|
+
}
|
135
|
+
end
|
136
|
+
packages.group_by { |package| package[:name] } .each do |name, _packages|
|
137
|
+
api_call :content_view_filter_rules,
|
138
|
+
:create,
|
139
|
+
{ :content_view_filter_id => cw_filter['id'],
|
140
|
+
:name => name}
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def import_single_row(data)
|
145
|
+
org_id = data['org_id'].to_i
|
146
|
+
|
147
|
+
repo_ids, clone_parents, packages, has_local = load_custom_channel_info org_id, data['channel_id'].to_i
|
148
|
+
|
149
|
+
repo_ids.map! { |id| get_translated_id :repositories, id.to_i }
|
150
|
+
|
151
|
+
if has_local
|
152
|
+
local_repo = add_local_repo data
|
153
|
+
sync_repo local_repo unless repo_synced? local_repo
|
154
|
+
repo_ids.push local_repo['id'].to_i
|
155
|
+
end
|
156
|
+
|
157
|
+
clone_parents.collect { |x| Integer(x) } .each do |parent_id|
|
158
|
+
begin
|
159
|
+
begin
|
160
|
+
parent_cv = get_cache(:redhat_content_views)[get_translated_id :redhat_content_views, [org_id, parent_id]]
|
161
|
+
rescue
|
162
|
+
parent_cv = get_cache(:content_views)[get_translated_id :content_views, parent_id]
|
163
|
+
end
|
164
|
+
repo_ids += parent_cv['repositories'].collect { |x| x['id'] }
|
165
|
+
rescue HammerCLIImport::MissingObjectError
|
166
|
+
error "No such {redhat_,}content_view: #{parent_id}"
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
repo_ids.collect { |id| lookup_entity :repositories, id } .each do |repo|
|
171
|
+
unless repo_synced? repo
|
172
|
+
warn "Repository #{repo['label']} is not (fully) synchronized. Retry once synchronization has completed."
|
173
|
+
report_summary :skipped, :content_views
|
174
|
+
return nil
|
175
|
+
end
|
176
|
+
end
|
177
|
+
content_view = mk_content_view_hash data, repo_ids
|
178
|
+
|
179
|
+
cw = create_entity :content_views, content_view, data['channel_id'].to_i
|
180
|
+
add_repo_filters cw['id'], packages if option_filter?
|
181
|
+
publish_content_view cw['id'] if newer_repositories cw
|
182
|
+
end
|
183
|
+
|
184
|
+
def delete_single_row(data)
|
185
|
+
cv_id = data['channel_id'].to_i
|
186
|
+
unless @pm[:content_views][cv_id] || @pm[:redhat_content_views][cv_id] || @pm[:system_content_views][cv_id]
|
187
|
+
info "#{to_singular(:content_views).capitalize} with id #{cv_id} wasn't imported. Skipping deletion."
|
188
|
+
return
|
189
|
+
end
|
190
|
+
translated = get_translated_id :content_views, cv_id
|
191
|
+
|
192
|
+
# delete_entity :content_views, cv_id
|
193
|
+
delete_content_view translated
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=ruby
|
@@ -0,0 +1,68 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 Red Hat Inc.
|
3
|
+
#
|
4
|
+
# This file is part of hammer-cli-import.
|
5
|
+
#
|
6
|
+
# hammer-cli-import is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# hammer-cli-import is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with hammer-cli-import. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'csv'
|
21
|
+
|
22
|
+
module CSVHelper
|
23
|
+
class CSVHelperError < RuntimeError
|
24
|
+
end
|
25
|
+
|
26
|
+
# Returns missing columns
|
27
|
+
def self.csv_missing_columns(filename, headers)
|
28
|
+
reader = CSV.open(filename, 'r')
|
29
|
+
real_header = reader.shift
|
30
|
+
reader.close
|
31
|
+
headers - real_header
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.csv_each(filename, headers)
|
35
|
+
raise CSVHelperError, 'Expecting block' unless block_given?
|
36
|
+
reader = CSV.open(filename, 'r')
|
37
|
+
real_header = reader.shift
|
38
|
+
raise CSVHelperError, "No header in #{filename}" if real_header.nil?
|
39
|
+
real_header_length = real_header.length
|
40
|
+
to_discard = real_header - headers
|
41
|
+
headers.each do |col|
|
42
|
+
raise CSVHelperError, "Column #{col} expected in #{filename}" unless real_header.include? col
|
43
|
+
end
|
44
|
+
reader.each do |row|
|
45
|
+
raise CSVHelperError, "Broken CSV in #{filename}: #{real_header_length} columns expected but found #{row.length}" \
|
46
|
+
unless row.length == real_header_length
|
47
|
+
data = Hash[real_header.zip row]
|
48
|
+
to_discard.each { |key| data.delete key }
|
49
|
+
class << data
|
50
|
+
def[](key)
|
51
|
+
raise CSVHelperError, "Referencing undeclared key: #{key}" unless key? key
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
yield data
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.csv_write_hashes(filename, headers, hashes)
|
60
|
+
CSV.open(filename, 'wb') do |csv|
|
61
|
+
csv << headers
|
62
|
+
hashes.each do |hash|
|
63
|
+
csv << headers.collect { |key| hash[key] }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=ruby
|
@@ -0,0 +1,86 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 Red Hat Inc.
|
3
|
+
#
|
4
|
+
# This file is part of hammer-cli-import.
|
5
|
+
#
|
6
|
+
# hammer-cli-import is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# hammer-cli-import is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with hammer-cli-import. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'set'
|
21
|
+
|
22
|
+
class DeltaHashError < RuntimeError
|
23
|
+
end
|
24
|
+
|
25
|
+
class DeltaHash
|
26
|
+
attr_reader :new
|
27
|
+
attr_reader :del
|
28
|
+
|
29
|
+
def self.[](hash)
|
30
|
+
new(hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(hash)
|
34
|
+
@old = hash
|
35
|
+
@new = {}
|
36
|
+
@del = Set.new
|
37
|
+
end
|
38
|
+
|
39
|
+
def [](key)
|
40
|
+
return nil if @del.include? key
|
41
|
+
@new[key] || @old[key]
|
42
|
+
end
|
43
|
+
|
44
|
+
def []=(key, val)
|
45
|
+
raise DeltaHashError, 'Key exists' if self[key]
|
46
|
+
@del.delete key
|
47
|
+
@new[key] = val unless @old[key] == val
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_hash
|
51
|
+
ret = (@old.merge @new)
|
52
|
+
@del.each do |key|
|
53
|
+
ret.delete key
|
54
|
+
end
|
55
|
+
ret
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete(key)
|
59
|
+
raise DeltaHashError, "Key #{key} does not exist" unless self[key]
|
60
|
+
@del << key if @old[key]
|
61
|
+
@new.delete(key)
|
62
|
+
end
|
63
|
+
|
64
|
+
def delete_value(value)
|
65
|
+
deleted = 0
|
66
|
+
to_hash.each do |k, v|
|
67
|
+
next unless v == value
|
68
|
+
delete(k)
|
69
|
+
deleted += 1
|
70
|
+
end
|
71
|
+
return deleted
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
to_hash.to_s
|
76
|
+
end
|
77
|
+
|
78
|
+
def inspect
|
79
|
+
to_hash.inspect
|
80
|
+
end
|
81
|
+
|
82
|
+
def changed?
|
83
|
+
! (@new.empty? && del.empty?)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=ruby
|
@@ -0,0 +1,27 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 Red Hat Inc.
|
3
|
+
#
|
4
|
+
# This file is part of hammer-cli-import.
|
5
|
+
#
|
6
|
+
# hammer-cli-import is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# hammer-cli-import is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with hammer-cli-import. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'time'
|
21
|
+
|
22
|
+
class Time
|
23
|
+
def iso8601
|
24
|
+
strftime '%Y-%m-%dT%H:%M:%S%z'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=ruby
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 Red Hat Inc.
|
3
|
+
#
|
4
|
+
# This file is part of hammer-cli-import.
|
5
|
+
#
|
6
|
+
# hammer-cli-import is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# hammer-cli-import is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with hammer-cli-import. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'hammer_cli'
|
21
|
+
require 'apipie-bindings'
|
22
|
+
|
23
|
+
module HammerCLIImport
|
24
|
+
class ImportCommand
|
25
|
+
class SystemGroupImportCommand < BaseCommand
|
26
|
+
command_name 'host-collection'
|
27
|
+
reportname = 'system-groups'
|
28
|
+
desc "Import Host Collections (from spacewalk-report #{reportname})."
|
29
|
+
|
30
|
+
csv_columns 'group_id', 'name', 'org_id'
|
31
|
+
|
32
|
+
persistent_maps :organizations, :host_collections
|
33
|
+
|
34
|
+
def mk_sg_hash(data)
|
35
|
+
{
|
36
|
+
:name => data['name'],
|
37
|
+
:organization_id => get_translated_id(:organizations, data['org_id'].to_i)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
def import_single_row(data)
|
42
|
+
sg = mk_sg_hash data
|
43
|
+
create_entity(:host_collections, sg, data['group_id'].to_i)
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete_single_row(data)
|
47
|
+
delete_entity(:host_collections, data['group_id'].to_i)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=ruby
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2014 Red Hat Inc.
|
3
|
+
#
|
4
|
+
# This file is part of hammer-cli-import.
|
5
|
+
#
|
6
|
+
# hammer-cli-import is free software: you can redistribute it and/or modify
|
7
|
+
# it under the terms of the GNU General Public License as published by
|
8
|
+
# the Free Software Foundation, either version 3 of the License, or
|
9
|
+
# (at your option) any later version.
|
10
|
+
#
|
11
|
+
# hammer-cli-import is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
14
|
+
# GNU General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU General Public License
|
17
|
+
# along with hammer-cli-import. If not, see <http://www.gnu.org/licenses/>.
|
18
|
+
#
|
19
|
+
|
20
|
+
require 'hammer_cli'
|
21
|
+
require 'hammer_cli/exit_codes'
|
22
|
+
|
23
|
+
module HammerCLIImport
|
24
|
+
class ImportCommand < HammerCLI::AbstractCommand
|
25
|
+
end
|
26
|
+
|
27
|
+
HammerCLI::MainCommand.subcommand('import',
|
28
|
+
'Import data exported from a Red Hat Satellite 5 instance',
|
29
|
+
HammerCLIImport::ImportCommand)
|
30
|
+
end
|
31
|
+
# vim: autoindent tabstop=2 shiftwidth=2 expandtab softtabstop=2 filetype=ruby
|