hosttag 0.12
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/ChangeLog +44 -0
- data/EXCLUDE +3 -0
- data/LICENCE +674 -0
- data/README +92 -0
- data/Rakefile +30 -0
- data/TODO +4 -0
- data/bin/hosttag +168 -0
- data/bin/ht +168 -0
- data/bin/htdel +164 -0
- data/bin/htdump +70 -0
- data/bin/htexport +99 -0
- data/bin/htimport +81 -0
- data/bin/htremap +93 -0
- data/bin/htset +164 -0
- data/etc/Makefile +16 -0
- data/etc/README +10 -0
- data/hosttag.gemspec +23 -0
- data/hosttag.spec +179 -0
- data/lib/hosttag.rb +402 -0
- data/lib/hosttag/server.rb +35 -0
- data/test/data_hosttag/a/centos +0 -0
- data/test/data_hosttag/a/centos5 +0 -0
- data/test/data_hosttag/a/centos5-x86_64 +0 -0
- data/test/data_hosttag/a/public +0 -0
- data/test/data_hosttag/g/SKIP +0 -0
- data/test/data_hosttag/g/centos +0 -0
- data/test/data_hosttag/g/centos4 +0 -0
- data/test/data_hosttag/g/centos4-i386 +0 -0
- data/test/data_hosttag/h/SKIP +0 -0
- data/test/data_hosttag/h/centos +0 -0
- data/test/data_hosttag/h/centos4 +0 -0
- data/test/data_hosttag/h/centos4-x86_64 +0 -0
- data/test/data_hosttag/h/public +0 -0
- data/test/data_hosttag/m/centos +0 -0
- data/test/data_hosttag/m/centos4 +0 -0
- data/test/data_hosttag/m/centos4-x86_64 +0 -0
- data/test/data_hosttag/m/public +0 -0
- data/test/data_hosttag/m/vps +0 -0
- data/test/data_hosttag/n/centos +0 -0
- data/test/data_hosttag/n/centos5 +0 -0
- data/test/data_hosttag/n/centos5-i386 +0 -0
- data/test/data_hosttag/n/laptop +0 -0
- data/test/test_hosttag_bin.rb +70 -0
- data/test/test_hosttag_lib.rb +119 -0
- data/test/test_htset_bin.rb +174 -0
- data/test/test_htset_lib.rb +183 -0
- data/test/ts_all.rb +4 -0
- metadata +132 -0
data/bin/htset
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Hosttag update client, redis version
|
4
|
+
#
|
5
|
+
# Usage:
|
6
|
+
# htset <host1> [<host2> ...] <tag> [<tag2> ...]
|
7
|
+
# htdel <host1> [<host2> ...] <tag> [<tag2> ...]
|
8
|
+
#
|
9
|
+
|
10
|
+
require 'optparse'
|
11
|
+
require 'fileutils'
|
12
|
+
require 'pp'
|
13
|
+
|
14
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
15
|
+
require 'hosttag'
|
16
|
+
include Hosttag
|
17
|
+
|
18
|
+
# ------------------------------------------------------------------------------
|
19
|
+
# Subroutines
|
20
|
+
|
21
|
+
def die(error)
|
22
|
+
puts error
|
23
|
+
exit 1
|
24
|
+
end
|
25
|
+
|
26
|
+
def parse_options(me)
|
27
|
+
options = { :all => false, :autoconfirm => false }
|
28
|
+
opts = OptionParser.new
|
29
|
+
opts.banner = "Usage: #{me} [options] <host> [<host2> ...] <tag> [<tag2>...]"
|
30
|
+
opts.on('-?', '-h', '--help') do
|
31
|
+
puts opts
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
opts.on('-h', '--help', '-?', 'Show this usage information') do
|
35
|
+
die(opts)
|
36
|
+
end
|
37
|
+
opts.on('-A', '--all', '(htdel) Delete all tags from hosts') do
|
38
|
+
options[:all] = true
|
39
|
+
end
|
40
|
+
opts.on('-y', '--yes', "(htdel) Don't ask for confirmation on delete all operations") do
|
41
|
+
options[:autoconfirm] = true
|
42
|
+
end
|
43
|
+
opts.on('-H', '--host', '--hosts', 'Treat unrecognised elements as hosts') do
|
44
|
+
options[:host_mode] = true
|
45
|
+
end
|
46
|
+
opts.on('-T', '--tag', '--tags', 'Treat unrecognised elements as tags') do
|
47
|
+
options[:tag_mode] = true
|
48
|
+
end
|
49
|
+
opts.on('--ns=STR', '--namespace=STR', String, 'Namespace into which we load hosttag data. Default: hosttag') do |val|
|
50
|
+
options[:namespace] = val
|
51
|
+
end
|
52
|
+
opts.on('-s=ARG', '--server=ARG', String, 'Server hostname to connect to') do |val|
|
53
|
+
options[:server] = val
|
54
|
+
end
|
55
|
+
opts.on('-p=ARG', '--port=ARG', Integer, 'Server port to connect to') do |val|
|
56
|
+
options[:port] = val
|
57
|
+
end
|
58
|
+
opts.on('-v', '--verbose', 'Verbose output') do
|
59
|
+
options[:verbose] = true
|
60
|
+
end
|
61
|
+
|
62
|
+
# Parse options
|
63
|
+
begin
|
64
|
+
args = opts.parse(ARGV)
|
65
|
+
rescue => e
|
66
|
+
die(opts)
|
67
|
+
end
|
68
|
+
|
69
|
+
if args.length < 2 and not options[:all]
|
70
|
+
die(opts)
|
71
|
+
end
|
72
|
+
if options[:all] and me =~ /set$/
|
73
|
+
warn "Error: --all not available with #{me}"
|
74
|
+
die(opts)
|
75
|
+
end
|
76
|
+
if options[:host_mode] and options[:tag_mode]
|
77
|
+
warn "Error: --host and --tag options are mutually exclusive"
|
78
|
+
die(opts)
|
79
|
+
end
|
80
|
+
|
81
|
+
return options, args
|
82
|
+
end
|
83
|
+
|
84
|
+
# Classify args into hosts, tags, and uncertain buckets
|
85
|
+
def classify_args(args, options)
|
86
|
+
results = { :host => [], :tag => [], :uncertain => [] }
|
87
|
+
verbose = options[:verbose]
|
88
|
+
|
89
|
+
# First arg must be host, and last tag, by definition
|
90
|
+
results[:host].push(args.shift)
|
91
|
+
last_tag = args.pop
|
92
|
+
|
93
|
+
# Classify remainder by doing lookups
|
94
|
+
while a = args.shift do
|
95
|
+
begin
|
96
|
+
tags = hosttag_lookup_hosts(a, options)
|
97
|
+
if tags.length > 0
|
98
|
+
# if 'a' is a valid host, then everything already in uncertain must be too
|
99
|
+
if results[:uncertain].length > 0
|
100
|
+
results[:host].push(*results[:uncertain])
|
101
|
+
results[:uncertain] = []
|
102
|
+
end
|
103
|
+
results[:host].push(a)
|
104
|
+
end
|
105
|
+
rescue
|
106
|
+
# 'a' is not a known host, check if a tag
|
107
|
+
begin
|
108
|
+
hosts = hosttag_lookup_tags(a, options)
|
109
|
+
if hosts.length > 0
|
110
|
+
# If 'a' is a valid tag, then everything else in args must be too
|
111
|
+
results[:tag].push(a, *args)
|
112
|
+
args = []
|
113
|
+
end
|
114
|
+
rescue
|
115
|
+
# 'a' is not a known host or tag, add to uncertain list
|
116
|
+
results[:uncertain].push(a)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
results[:tag].push(last_tag)
|
122
|
+
|
123
|
+
return results
|
124
|
+
end
|
125
|
+
|
126
|
+
# ------------------------------------------------------------------------------
|
127
|
+
# Main
|
128
|
+
|
129
|
+
mode = $0.sub(/^.*\//, '')
|
130
|
+
|
131
|
+
options, args = parse_options(mode)
|
132
|
+
|
133
|
+
# Normal mode
|
134
|
+
if (not options[:all])
|
135
|
+
results = classify_args(args, options)
|
136
|
+
if options[:verbose]
|
137
|
+
print "+ results: "
|
138
|
+
pp results
|
139
|
+
end
|
140
|
+
|
141
|
+
if results[:uncertain].length > 0
|
142
|
+
# --hosts: treat unknown elements as hosts
|
143
|
+
if options[:host_mode]
|
144
|
+
results[:host].push(*results[:uncertain])
|
145
|
+
elsif options[:tag_mode]
|
146
|
+
results[:tag].push(*results[:uncertain])
|
147
|
+
else
|
148
|
+
# TODO: do something useful here - ask the user?
|
149
|
+
die("Error: can't auto-classify '#{results[:uncertain].join(',')}' - aborting")
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if (mode =~ /del$/)
|
154
|
+
hosttag_delete_tags(results[:host], results[:tag], options)
|
155
|
+
else
|
156
|
+
hosttag_add_tags(results[:host], results[:tag], options)
|
157
|
+
end
|
158
|
+
|
159
|
+
# htdel --all mode
|
160
|
+
elsif (mode =~ /del$/)
|
161
|
+
hosttag_delete_all_tags(args, options)
|
162
|
+
|
163
|
+
end
|
164
|
+
|
data/etc/Makefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
all: import export
|
2
|
+
|
3
|
+
import:
|
4
|
+
# redis has essentially open write permissions, so htimport is limited to root
|
5
|
+
sudo /usr/sbin/htimport
|
6
|
+
|
7
|
+
export:
|
8
|
+
# htexport is already limited by filesystem permissions, so doesn't need to be root
|
9
|
+
/usr/bin/htexport
|
10
|
+
|
11
|
+
import-delete:
|
12
|
+
sudo /usr/sbin/htimport --delete
|
13
|
+
|
14
|
+
export-delete:
|
15
|
+
/usr/bin/htexport --delete
|
16
|
+
|
data/etc/README
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
To add hosttags, create host directories in the /etc/hosttag directory,
|
2
|
+
and touch tag files within the host directories e.g.
|
3
|
+
|
4
|
+
cd /etc/hosttag
|
5
|
+
mkdir host1 host2 host3
|
6
|
+
touch host1/{tag1,tag2,tag3}
|
7
|
+
touch host2/{tag1,tag2,tag3}
|
8
|
+
|
9
|
+
and then run 'make' to update the hosttag database.
|
10
|
+
|
data/hosttag.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "hosttag"
|
3
|
+
s.version = "0.12"
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
s.authors = ["Gavin Carr"]
|
6
|
+
s.email = ["gavin@openfusion.net"]
|
7
|
+
s.homepage = "http://github.com/gavincarr/hosttag"
|
8
|
+
s.summary = "Hosttag is a client for tagging hostnames into classes using a redis datastore"
|
9
|
+
s.description = "Hosttag is a client for tagging hostnames into groups or classes,
|
10
|
+
storing them in a redis datastore"
|
11
|
+
s.rubyforge_project = s.name
|
12
|
+
|
13
|
+
s.required_rubygems_version = ">= 1.3.6"
|
14
|
+
|
15
|
+
# If you have runtime dependencies, add them here
|
16
|
+
s.add_runtime_dependency "redis", "~> 2.0"
|
17
|
+
|
18
|
+
# The list of files to be contained in the gem
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
|
21
|
+
s.require_path = 'lib'
|
22
|
+
end
|
23
|
+
|
data/hosttag.spec
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
%define ruby_sitelib %(ruby -rrbconfig -e "puts Config::CONFIG['sitelibdir']")
|
2
|
+
|
3
|
+
Summary: Hosttag client
|
4
|
+
Name: hosttag
|
5
|
+
Version: 0.12
|
6
|
+
Release: 1%{org_tag}%{dist}
|
7
|
+
URL: http://www.openfusion.com.au/labs/
|
8
|
+
Source0: http://www.openfusion.com.au/labs/dist/%{name}-%{version}.tar.gz
|
9
|
+
License: GPL
|
10
|
+
Group: Applications/System
|
11
|
+
BuildRoot: %{_tmppath}/%{name}-%{version}
|
12
|
+
BuildArch: noarch
|
13
|
+
Requires: rubygems, rubygem-redis >= 2.0.0
|
14
|
+
|
15
|
+
%description
|
16
|
+
Hosttag is a client for tagging hostnames into groups or classes, storing
|
17
|
+
the mappings in a redis datastore.
|
18
|
+
|
19
|
+
This package contains the hosttag client utilities.
|
20
|
+
|
21
|
+
%package server-utils
|
22
|
+
Summary: Hosttag server utilities
|
23
|
+
Group: Applications/System
|
24
|
+
Requires: hosttag = %version
|
25
|
+
Requires: redis, rubygems, rubygem-redis >= 2.0
|
26
|
+
Obsoletes: hosttag-server
|
27
|
+
Conflicts: hosttag-server
|
28
|
+
|
29
|
+
%description server-utils
|
30
|
+
Hosttag is a client for tagging hostnames into groups or classes, storing
|
31
|
+
the mappings in a redis datastore.
|
32
|
+
|
33
|
+
This package contains optional hosttag server utilities, allowing you to
|
34
|
+
export and import hosttag mappings to disk. It's not required for normal
|
35
|
+
use, however.
|
36
|
+
|
37
|
+
%prep
|
38
|
+
%setup
|
39
|
+
|
40
|
+
%build
|
41
|
+
|
42
|
+
%install
|
43
|
+
test "%{buildroot}" != "/" && rm -rf %{buildroot}
|
44
|
+
|
45
|
+
mkdir -p %{buildroot}%{ruby_sitelib}/hosttag
|
46
|
+
install -m0644 lib/hosttag.rb %{buildroot}%{ruby_sitelib}/hosttag.rb
|
47
|
+
install -m0644 lib/hosttag/server.rb %{buildroot}%{ruby_sitelib}/hosttag
|
48
|
+
|
49
|
+
mkdir -p %{buildroot}%{_bindir}
|
50
|
+
install -m0755 bin/hosttag %{buildroot}%{_bindir}/hosttag
|
51
|
+
install -m0755 bin/htexport %{buildroot}%{_bindir}/htexport
|
52
|
+
|
53
|
+
# htset and htimport are executable by root only, to restrict tagging to root
|
54
|
+
install -m0700 bin/htset %{buildroot}%{_bindir}/htset
|
55
|
+
install -m0700 bin/htimport %{buildroot}%{_bindir}/htimport
|
56
|
+
#install -m0700 bin/htdump %{buildroot}%{_bindir}/htdump
|
57
|
+
|
58
|
+
mkdir -p %{buildroot}%{_sysconfdir}/%{name}
|
59
|
+
install -m0644 etc/Makefile %{buildroot}%{_sysconfdir}/%{name}
|
60
|
+
install -m0644 etc/README %{buildroot}%{_sysconfdir}/%{name}
|
61
|
+
|
62
|
+
cd %{buildroot}%{_bindir}
|
63
|
+
ln -s hosttag ht
|
64
|
+
cd %{buildroot}%{_bindir}
|
65
|
+
ln -s htset htdel
|
66
|
+
|
67
|
+
%clean
|
68
|
+
test "%{buildroot}" != "/" && rm -rf %{buildroot}
|
69
|
+
|
70
|
+
%files
|
71
|
+
%defattr(-,root,root)
|
72
|
+
%{ruby_sitelib}/hosttag.rb
|
73
|
+
%{ruby_sitelib}/hosttag/*
|
74
|
+
%{_bindir}/hosttag
|
75
|
+
%{_bindir}/ht
|
76
|
+
%attr(0700,root,root) %{_bindir}/htset
|
77
|
+
%{_bindir}/htdel
|
78
|
+
#%attr(0700,root,root) %{_bindir}/htdump
|
79
|
+
%doc README LICENCE
|
80
|
+
|
81
|
+
%files server-utils
|
82
|
+
%defattr(-,root,root)
|
83
|
+
%config(noreplace) %{_sysconfdir}/%{name}/Makefile
|
84
|
+
%{_sysconfdir}/%{name}/README
|
85
|
+
%attr(0755,root,root) %{_bindir}/htexport
|
86
|
+
%attr(0700,root,root) %{_bindir}/htimport
|
87
|
+
|
88
|
+
%changelog
|
89
|
+
* Mon Nov 21 2011 Gavin Carr <gavin@openfusion.com.au> 0.12
|
90
|
+
- Cleanups and tweaks for better compatibility with ruby 1.9.x.
|
91
|
+
|
92
|
+
* Wed Feb 02 2011 Gavin Carr <gavin@openfusion.com.au> 0.11
|
93
|
+
* Add Hosttag::Server support for HOSTTAG_{SERVER,PORT,NAMESPACE} env variables.
|
94
|
+
- Rename hosttag-server subpackage to hosttag-server-utils.
|
95
|
+
|
96
|
+
* Thu Jan 13 2011 Gavin Carr <gavin@openfusion.com.au> 0.10.5
|
97
|
+
- Remove htdump from spec file, since it clashes with htdig utility.
|
98
|
+
|
99
|
+
* Wed Jan 12 2011 Gavin Carr <gavin@openfusion.com.au> 0.10.4
|
100
|
+
- Add support for hosttag -A -l and -T -l.
|
101
|
+
|
102
|
+
* Wed Jan 12 2011 Gavin Carr <gavin@openfusion.com.au> 0.10.3
|
103
|
+
- Fix hosttag.render to handle nil results better.
|
104
|
+
|
105
|
+
* Tue Jan 11 2011 Gavin Carr <gavin@openfusion.com.au> 0.10.2
|
106
|
+
- Fix some buglets in htset.
|
107
|
+
- Move root utils from %{_sbindir} to %{_bindir}.
|
108
|
+
|
109
|
+
* Fri Jan 07 2011 Gavin Carr <gavin@openfusion.com.au> 0.10.1
|
110
|
+
- Fix a couple of small bugs in 0.10.
|
111
|
+
|
112
|
+
* Thu Jan 06 2011 Gavin Carr <gavin@openfusion.com.au> 0.10
|
113
|
+
- Librification release, creating new Hosttag module with core functionality.
|
114
|
+
- Rewrite hosttag, htset, and htimport to use new Hosttag module.
|
115
|
+
- Rewrite hosttag_{add,delete}_tags routines to handle tricksy SKIP corner cases.
|
116
|
+
- Create lib versions of unit tests alongside existing bin ones.
|
117
|
+
- Expand unit test coverage for htset/htdel.
|
118
|
+
- Update unit tests to use library calls instead of calling out to utils.
|
119
|
+
- Change all_{hosts,tags}_* key names for greater clarity.
|
120
|
+
|
121
|
+
* Mon May 24 2010 Gavin Carr <gavin@openfusion.com.au> 0.9
|
122
|
+
- Migrate redis api calls over to redis 2.0.0 gem.
|
123
|
+
- Add --all option to htdel, for deleting all tags from a host.
|
124
|
+
- Add --host and --tag option to htset for ambiguous elements.
|
125
|
+
|
126
|
+
* Fri Feb 12 2010 Gavin Carr <gavin@openfusion.com.au> 0.8.1
|
127
|
+
- Add a -1 argument to hosttag to list results one per line.
|
128
|
+
|
129
|
+
* Mon Feb 08 2010 Gavin Carr <gavin@openfusion.com.au> 0.8
|
130
|
+
- Refactor, pulling server bits into Hosttag::Server.
|
131
|
+
- Add htset unit test, and fix bugs arising.
|
132
|
+
|
133
|
+
* Mon Feb 08 2010 Gavin Carr <gavin@openfusion.com.au> 0.7.1
|
134
|
+
- Fix typo in htset.
|
135
|
+
|
136
|
+
* Fri Feb 05 2010 Gavin Carr <gavin@openfusion.com.au> 0.7
|
137
|
+
- Add namespace option to all binaries.
|
138
|
+
- Add options to htdump.
|
139
|
+
- Add missing htdump to spec file.
|
140
|
+
|
141
|
+
* Wed Feb 03 2010 Gavin Carr <gavin@openfusion.com.au> 0.6.9
|
142
|
+
- Add missing htdel symlink to hosttag package.
|
143
|
+
|
144
|
+
* Tue Feb 02 2010 Gavin Carr <gavin@openfusion.com.au> 0.6.8
|
145
|
+
- Fix bug with htset not deleting host from noskip list if SKIP tag set.
|
146
|
+
|
147
|
+
* Wed Jan 13 2010 Gavin Carr <gavin@openfusion.com.au> 0.6.7
|
148
|
+
- Add --list mode to hosttag.
|
149
|
+
|
150
|
+
* Thu Dec 31 2009 Gavin Carr <gavin@openfusion.com.au> 0.6.6
|
151
|
+
- Rename hosttag_export to htexport, and hosttag_load_data to htimport.
|
152
|
+
- Change old --import parameter to htimport to --delete, like htexport.
|
153
|
+
- Make htimport more verbose, like htexport.
|
154
|
+
|
155
|
+
* Tue Dec 29 2009 Gavin Carr <gavin@openfusion.com.au> 0.6.5
|
156
|
+
- Add hosttag_export utility to export redis db back to directory tree.
|
157
|
+
|
158
|
+
* Tue Dec 08 2009 Gavin Carr <gavin@openfusion.com.au> 0.6
|
159
|
+
- Move from tokyo cabinet/tyrant server to redis-based one.
|
160
|
+
- Rewrite client in ruby.
|
161
|
+
|
162
|
+
* Wed Nov 04 2009 Gavin Carr <gavin@openfusion.com.au> 0.5
|
163
|
+
- Fixes to hosttag_load_data.
|
164
|
+
- Add SKIP tag support to hosttag_load_data.
|
165
|
+
- Mode fixes to hosttag.
|
166
|
+
- Add htserver init scripts to hosttag-server package.
|
167
|
+
|
168
|
+
* Thu Oct 01 2009 Gavin Carr <gavin@openfusion.com.au> 0.4
|
169
|
+
- Rename data to etc, and load_data to hosttag_load_data.
|
170
|
+
- Add -server subpackage to hosttag.spec.
|
171
|
+
|
172
|
+
* Thu Oct 01 2009 Gavin Carr <gavin@openfusion.com.au> 0.3
|
173
|
+
- Change -h|--hosts parameters to -t|--tags (and deprecate -h).
|
174
|
+
- Allow bare 'ht -t' for listing all tags.
|
175
|
+
- Add default rel for multitag and multihost queries.
|
176
|
+
|
177
|
+
* Thu Feb 19 2009 Gavin Carr <gavin@openfusion.com.au> 0.1
|
178
|
+
- Initial package, version 0.1.
|
179
|
+
|
data/lib/hosttag.rb
ADDED
@@ -0,0 +1,402 @@
|
|
1
|
+
|
2
|
+
require 'hosttag/server'
|
3
|
+
|
4
|
+
module Hosttag
|
5
|
+
|
6
|
+
# Lookup the given tag(s), returning an array of hosts to which they apply.
|
7
|
+
# If multiple tags are given, by default the list of hosts is those to
|
8
|
+
# which ALL of the tags apply i.e. results are ANDed or intersected. To
|
9
|
+
# change this, pass :rel => :or in the options hash.
|
10
|
+
# The final argument may be an options hash, which accepts the following
|
11
|
+
# keys:
|
12
|
+
# - :rel - either :and or :or, specifying the relationship to use when
|
13
|
+
# interpreting the set of tags. :rel => :and returns the set of hosts to
|
14
|
+
# which ALL the given tags apply; :rel => :or returns the set of hosts
|
15
|
+
# to which ANY of the tags apply. Default: :rel => :and.
|
16
|
+
# - :include_skip? - flag indicating whether to include hosts that have
|
17
|
+
# the SKIP tag set. Default: false i.e. omit hosts tagged with SKIP.
|
18
|
+
def hosttag_lookup_tags(*args)
|
19
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
20
|
+
return lookup_keys(args, options.merge({ :type => :tag }))
|
21
|
+
end
|
22
|
+
|
23
|
+
# Lookup the given host(s), returning an array of tags that apply to
|
24
|
+
# them. If multiple hosts are given, by default the list of tags is
|
25
|
+
# those applying to ANY of the given hosts i.e. the results are ORed or
|
26
|
+
# unioned. To change this pass an explicit :rel => :and in the options
|
27
|
+
# hash.
|
28
|
+
# The final argument may be an options hash, which accepts the following
|
29
|
+
# keys:
|
30
|
+
# - :rel - either :and or :or, specifying the relationship to use when
|
31
|
+
# interpreting the set of hosts. :rel => :and returns the set of tags
|
32
|
+
# that apply to ALL the given hosts; :rel => :or returns the set of tags
|
33
|
+
# that apply to ANY of the given hosts. Default: :rel => :or.
|
34
|
+
# - :include_skip? - flag indicating whether to include hosts that have
|
35
|
+
# the SKIP tag set. Default: false i.e. omit hosts tagged with SKIP.
|
36
|
+
def hosttag_lookup_hosts(*args)
|
37
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
38
|
+
return lookup_keys(args, options.merge({ :type => :host }))
|
39
|
+
end
|
40
|
+
|
41
|
+
# Lookup the given host(s) or tag(s), returning an array of tags or hosts,
|
42
|
+
# as appropriate. If a :type option is not explicitly given, first tries
|
43
|
+
# the lookup using hosttag_lookup_tags, and if that fails retries using
|
44
|
+
# hosttag_lookup_hosts.
|
45
|
+
# The final argument may be an options hash, which accepts the following
|
46
|
+
# keys:
|
47
|
+
# - :type - either :host or :tag, specifying how to interpret the given
|
48
|
+
# arguments: :type => :host specifies that the arguments are hosts, and
|
49
|
+
# that the resultset should be a list of tags; :type => :tag specifies
|
50
|
+
# that the arguments are tags, and the resultset should be a list of
|
51
|
+
# hosts. Required, no default.
|
52
|
+
# - :rel - either :and or :or, specifying the relationship to use when
|
53
|
+
# interpreting the set of results. :rel => :and returns only results
|
54
|
+
# that have ALL of the given attributes i.e. the AND result set;
|
55
|
+
# :rel => :or returns results that have ANY of the given attributes
|
56
|
+
# i.e. the OR result set. Default: depends on :type - :and for :type
|
57
|
+
# => :host, and :or for :type => :tag.
|
58
|
+
# - :include_skip? - flag indicating whether to include hosts that have
|
59
|
+
# the SKIP tag set. Default: false i.e. omit hosts tagged with SKIP.
|
60
|
+
def hosttag_lookup(*args)
|
61
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
62
|
+
return lookup_keys(args, options) if options[:type]
|
63
|
+
|
64
|
+
begin
|
65
|
+
return hosttag_lookup_tags(args, options)
|
66
|
+
rescue => e
|
67
|
+
begin
|
68
|
+
return hosttag_lookup_hosts(args, options)
|
69
|
+
rescue
|
70
|
+
# If both lookups failed, re-raise original error
|
71
|
+
raise e
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return an array of all hosts
|
77
|
+
# The final argument may be an options hash, which accepts the following
|
78
|
+
# keys:
|
79
|
+
# - :include_skip? - flag indicating whether to include hosts that have
|
80
|
+
# the SKIP tag set. Default: false i.e. omit hosts tagged with SKIP.
|
81
|
+
def hosttag_all_hosts(options)
|
82
|
+
r = hosttag_server(options)
|
83
|
+
key = r.get_key(options[:include_skip?] ? 'all_hosts_full' : 'all_hosts')
|
84
|
+
$stderr.puts "+ key: #{key}" if options[:debug]
|
85
|
+
return r.smembers(key).sort
|
86
|
+
end
|
87
|
+
|
88
|
+
# Return an array of all tags
|
89
|
+
# The final argument may be an options hash, which accepts the following
|
90
|
+
# keys:
|
91
|
+
# - :include_skip? - flag indicating whether to include the SKIP tag.
|
92
|
+
# Default: false. Included for completeness.
|
93
|
+
def hosttag_all_tags(options)
|
94
|
+
r = hosttag_server(options)
|
95
|
+
key = r.get_key(options[:include_skip?] ? 'all_tags_full' : 'all_tags')
|
96
|
+
$stderr.puts "+ key: #{key}" if options[:debug]
|
97
|
+
return r.smembers(key).sort
|
98
|
+
end
|
99
|
+
|
100
|
+
# Add the given tags to all the given hosts
|
101
|
+
def hosttag_add_tags(hosts, tags, options)
|
102
|
+
r = hosttag_server(options)
|
103
|
+
|
104
|
+
# Add tags to each host
|
105
|
+
skip_host = {}
|
106
|
+
all_hosts_skip_hosts = true
|
107
|
+
hosts.each do |host|
|
108
|
+
key = r.get_key('host', host)
|
109
|
+
tags.each { |tag| r.sadd(key, tag) }
|
110
|
+
|
111
|
+
if r.sismember(key, 'SKIP')
|
112
|
+
skip_host[host] = true
|
113
|
+
else
|
114
|
+
all_hosts_skip_hosts = false
|
115
|
+
end
|
116
|
+
|
117
|
+
# Add to all_hosts sets
|
118
|
+
all_hosts = r.get_key('all_hosts')
|
119
|
+
all_hosts_full = r.get_key('all_hosts_full')
|
120
|
+
# all_hosts shouldn't include SKIP hosts, so those we remove
|
121
|
+
if skip_host[host]
|
122
|
+
r.srem(all_hosts, host)
|
123
|
+
else
|
124
|
+
r.sadd(all_hosts, host)
|
125
|
+
end
|
126
|
+
r.sadd(all_hosts_full, host)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Add hosts to each tag
|
130
|
+
recheck_for_skip = false
|
131
|
+
tags.each do |tag|
|
132
|
+
# If we've added a SKIP tag to these hosts, flag to do some extra work
|
133
|
+
recheck_for_skip = true if tag == 'SKIP'
|
134
|
+
|
135
|
+
key = r.get_key('tag', tag)
|
136
|
+
hosts.each do |host|
|
137
|
+
# The standard case is to add the host to the list for this tag.
|
138
|
+
# But we don't want SKIP hosts being included in these lists, so
|
139
|
+
# for them we actually do a remove to make sure they're omitted.
|
140
|
+
if skip_host[host] and tag != 'SKIP'
|
141
|
+
r.srem(key, host)
|
142
|
+
else
|
143
|
+
r.sadd(key, host)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Add to all_tags sets
|
148
|
+
all_tags = r.get_key('all_tags')
|
149
|
+
all_tags_full = r.get_key('all_tags_full')
|
150
|
+
r.sadd(all_tags, tag) unless all_hosts_skip_hosts
|
151
|
+
r.sadd(all_tags_full, tag)
|
152
|
+
end
|
153
|
+
|
154
|
+
# If we've added a SKIP tag here, we need to recheck all tags for all skip hosts
|
155
|
+
recheck_skip_change_for_all_tags(skip_host.keys, :add, r) if recheck_for_skip
|
156
|
+
end
|
157
|
+
|
158
|
+
# Delete the given tags from all the given hosts
|
159
|
+
def hosttag_delete_tags(hosts, tags, options)
|
160
|
+
r = hosttag_server(options)
|
161
|
+
|
162
|
+
# Delete tags from each host
|
163
|
+
non_skip_host = {}
|
164
|
+
hosts.each do |host|
|
165
|
+
key = r.get_key('host', host)
|
166
|
+
tags.each { |tag| r.srem(key, tag) }
|
167
|
+
|
168
|
+
if r.sismember(key, 'SKIP')
|
169
|
+
skip_host = true
|
170
|
+
else
|
171
|
+
non_skip_host[host] = true
|
172
|
+
end
|
173
|
+
|
174
|
+
# Delete from all_hosts sets
|
175
|
+
all_hosts = r.get_key('all_hosts')
|
176
|
+
all_hosts_full = r.get_key('all_hosts_full')
|
177
|
+
# If all tags have been deleted, or this is a SKIP host, remove from all_hosts
|
178
|
+
if r.scard(key) == 0 or skip_host
|
179
|
+
r.srem(all_hosts, host)
|
180
|
+
else
|
181
|
+
# NB: we explicitly add here in case we've deleted a SKIP tag
|
182
|
+
r.sadd(all_hosts, host)
|
183
|
+
end
|
184
|
+
if r.scard(key) == 0
|
185
|
+
r.srem(all_hosts_full, host)
|
186
|
+
r.del(key)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Delete hosts from each tag
|
191
|
+
recheck_for_skip = false
|
192
|
+
all_tags = r.get_key('all_tags')
|
193
|
+
all_tags_full = r.get_key('all_tags_full')
|
194
|
+
tags.each do |tag|
|
195
|
+
# If we've deleted a SKIP tag from these hosts, flag to do some extra work
|
196
|
+
recheck_for_skip = true if tag == 'SKIP'
|
197
|
+
|
198
|
+
tag_key = r.get_key('tag', tag)
|
199
|
+
hosts.each { |host| r.srem(tag_key, host) }
|
200
|
+
|
201
|
+
# Delete from all_tags sets
|
202
|
+
# If all hosts have been deleted (or this is the SKIP tag), remove from all_tags
|
203
|
+
if r.scard(tag_key) == 0 or tag == 'SKIP'
|
204
|
+
r.srem(all_tags, tag)
|
205
|
+
else
|
206
|
+
# NB: we explicitly add here in case we've deleted a SKIP tag
|
207
|
+
r.sadd(all_tags, tag)
|
208
|
+
end
|
209
|
+
if r.scard(tag_key) == 0
|
210
|
+
r.srem(all_tags_full, tag)
|
211
|
+
r.del(tag_key)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
r.del(all_tags) if r.scard(all_tags) == 0
|
215
|
+
r.del(all_tags_full) if r.scard(all_tags_full) == 0
|
216
|
+
|
217
|
+
# If we've deleted a SKIP tag here, we need to recheck all tags for all non-skip hosts
|
218
|
+
recheck_skip_change_for_all_tags(non_skip_host.keys, :delete, r) if recheck_for_skip
|
219
|
+
end
|
220
|
+
|
221
|
+
# Delete all hosts and tags in the hosttag datastore. This is the nuclear option,
|
222
|
+
# used in hosttag_import_from_directory if :delete => true. Interactively confirms
|
223
|
+
# unless the :autoconfirm option is set.
|
224
|
+
# The final argument may be an options hash, which accepts the following
|
225
|
+
# keys:
|
226
|
+
# - :autoconfirm - if true, truncate without asking for any confirmation
|
227
|
+
def hosttag_truncate(options)
|
228
|
+
if not options[:autoconfirm]
|
229
|
+
print "Do you really want to delete EVERYTHING from your datastore? [yN] "
|
230
|
+
$stdout.flush
|
231
|
+
confirm = $stdin.gets.chomp
|
232
|
+
return unless confirm =~ %r{^y}i
|
233
|
+
end
|
234
|
+
|
235
|
+
r = hosttag_server(options)
|
236
|
+
r.keys(r.get_key("*")).each { |k| r.del(k) }
|
237
|
+
end
|
238
|
+
|
239
|
+
# Delete all tags from the given hosts. Interactively confirms the deletions
|
240
|
+
# unless the :autoconfirm option is set.
|
241
|
+
# The final argument may be an options hash, which accepts the following
|
242
|
+
# keys:
|
243
|
+
# - :autoconfirm - if true, do deletes without asking for any confirmation
|
244
|
+
def hosttag_delete_all_tags(hosts, options)
|
245
|
+
if not options[:autoconfirm]
|
246
|
+
host_str = hosts.join(' ')
|
247
|
+
print "Do you want to delete all tags on the following host(s):\n #{host_str}\nConfirm? [yN] "
|
248
|
+
$stdout.flush
|
249
|
+
confirm = $stdin.gets.chomp
|
250
|
+
return unless confirm =~ %r{^y}i
|
251
|
+
end
|
252
|
+
|
253
|
+
hosts.each do |host|
|
254
|
+
begin
|
255
|
+
tags = hosttag_lookup_hosts(host, options)
|
256
|
+
hosttag_delete_tags([ host ], tags, options)
|
257
|
+
rescue
|
258
|
+
warn "Warning: invalid host '#{host}' - cannot delete"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
# Import hosts and tags from the given directory. The directory is
|
264
|
+
# expected to contain a set of directories, representing hosts; each
|
265
|
+
# file within those directories is treated as a tag that applies to
|
266
|
+
# that host.
|
267
|
+
# Options is a hash which accepts the following keys:
|
268
|
+
# - :delete - if true, delete ALL hosts and tags from the datastore
|
269
|
+
# before doing the import.
|
270
|
+
# - :autoconfirm - if true, don't interactively confirm deletions
|
271
|
+
def hosttag_import_from_directory(datadir, options)
|
272
|
+
# Delete ALL hosts and tags from the datastore if options[:delete] set
|
273
|
+
hosttag_truncate(options) if options[:delete]
|
274
|
+
|
275
|
+
# Load directory into a { host => [ taglist ] } hash
|
276
|
+
host_tag_hash = load_directory(datadir, options)
|
277
|
+
|
278
|
+
# Add all hosts and tags
|
279
|
+
host_tag_hash.each do |host, tags|
|
280
|
+
hosttag_add_tags([ host ], tags, options)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
private
|
285
|
+
|
286
|
+
# Lookup the given keys in the redis datastore, returning an array of
|
287
|
+
# results. If more than one key is specified, resultsets are merged
|
288
|
+
# (either ANDed or ORed) depending on the value of the :rel option.
|
289
|
+
# The final argument must be an options hash, which accepts the
|
290
|
+
# following options:
|
291
|
+
# - :type - specifies the type of keys to lookup, either :host or :tag.
|
292
|
+
# Required.
|
293
|
+
# - :rel - specifies how to merge multiple resultsets, either :and (set
|
294
|
+
# intersection) or :or (set union).
|
295
|
+
def lookup_keys(*args)
|
296
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
297
|
+
args.flatten!
|
298
|
+
|
299
|
+
type = options[:type]
|
300
|
+
throw "Required option 'type' missing" if not type
|
301
|
+
rel = options[:rel]
|
302
|
+
|
303
|
+
r = hosttag_server(options)
|
304
|
+
|
305
|
+
# Default a rel if we have multiple args
|
306
|
+
if args.length > 1 and not rel
|
307
|
+
rel = (type == :tag ? :and : :or)
|
308
|
+
end
|
309
|
+
$stderr.puts "+ rel (#{type}): #{rel}" if rel and options[:debug]
|
310
|
+
|
311
|
+
# Map keys to fetch
|
312
|
+
keys = args.collect {|v| r.get_key(type, v) }
|
313
|
+
$stderr.puts "+ keys: #{keys.join(' ')}" if options[:debug]
|
314
|
+
|
315
|
+
# Check all keys exist
|
316
|
+
keys.each do |k|
|
317
|
+
if not r.exists(k)
|
318
|
+
item = k.sub(%r{^[^:]+::[^:]+:}, '')
|
319
|
+
raise "Error: #{type} '#{item}' not found."
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Lookup and return
|
324
|
+
if keys.length == 1
|
325
|
+
r.smembers(keys[0]).sort
|
326
|
+
elsif rel == :and
|
327
|
+
r.sinter(*keys).sort
|
328
|
+
else
|
329
|
+
r.sunion(*keys).sort
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# If we've added or removed a SKIP tag, we now have to recheck all tags for
|
334
|
+
# the given hosts, removing or re-adding them from/to those tag sets, and
|
335
|
+
# then recalculate the all_tags set for each of those tags
|
336
|
+
def recheck_skip_change_for_all_tags(hosts, change, r)
|
337
|
+
recheck_tags = {}
|
338
|
+
hosts.each do |host|
|
339
|
+
host_key = r.get_key('host', host)
|
340
|
+
r.smembers(host_key).each do |tag|
|
341
|
+
next if tag == 'SKIP'
|
342
|
+
|
343
|
+
tag_key = r.get_key('tag', tag)
|
344
|
+
# If we've added SKIP tags, then we remove the host from tagsets
|
345
|
+
# (or vice-versa)
|
346
|
+
if change == :add
|
347
|
+
r.srem(tag_key, host)
|
348
|
+
else
|
349
|
+
r.sadd(tag_key, host)
|
350
|
+
end
|
351
|
+
|
352
|
+
recheck_tags[tag] = tag_key
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# Now recheck the all_tags set, adding tags that have hosts, and
|
357
|
+
# removing any that don't
|
358
|
+
all_tags = r.get_key('all_tags')
|
359
|
+
recheck_tags.each do |tag, tag_key|
|
360
|
+
tag_host_count = r.scard(tag_key)
|
361
|
+
if tag_host_count == 0
|
362
|
+
r.srem(all_tags, tag)
|
363
|
+
else
|
364
|
+
r.sadd(all_tags, tag)
|
365
|
+
end
|
366
|
+
end
|
367
|
+
r.del(all_tags) if r.scard(all_tags) == 0
|
368
|
+
end
|
369
|
+
|
370
|
+
# Load all host/tag files in datadir, returning a { host => [ taglist ] } hash
|
371
|
+
def load_directory(datadir, options)
|
372
|
+
host_tag_hash = {}
|
373
|
+
|
374
|
+
Dir.glob("#{datadir}/*").each do |host_path|
|
375
|
+
next if not File.directory?(host_path)
|
376
|
+
host = host_path.sub(/^#{datadir}\//, '')
|
377
|
+
|
378
|
+
host_tag_hash[host] = []
|
379
|
+
Dir.glob("#{host_path}/*").each do |tag_path|
|
380
|
+
next if not File.file?(tag_path)
|
381
|
+
tag = File.basename(tag_path)
|
382
|
+
host_tag_hash[host].push(tag)
|
383
|
+
end
|
384
|
+
end
|
385
|
+
|
386
|
+
return host_tag_hash
|
387
|
+
end
|
388
|
+
|
389
|
+
def die(error)
|
390
|
+
warn error
|
391
|
+
exit 1
|
392
|
+
end
|
393
|
+
|
394
|
+
def hosttag_server(options)
|
395
|
+
begin
|
396
|
+
r = Hosttag::Server.new(options)
|
397
|
+
rescue Resolv::ResolvError => e
|
398
|
+
die e
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
end
|