hosttag 0.12

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.
Files changed (49) hide show
  1. data/.gitignore +2 -0
  2. data/ChangeLog +44 -0
  3. data/EXCLUDE +3 -0
  4. data/LICENCE +674 -0
  5. data/README +92 -0
  6. data/Rakefile +30 -0
  7. data/TODO +4 -0
  8. data/bin/hosttag +168 -0
  9. data/bin/ht +168 -0
  10. data/bin/htdel +164 -0
  11. data/bin/htdump +70 -0
  12. data/bin/htexport +99 -0
  13. data/bin/htimport +81 -0
  14. data/bin/htremap +93 -0
  15. data/bin/htset +164 -0
  16. data/etc/Makefile +16 -0
  17. data/etc/README +10 -0
  18. data/hosttag.gemspec +23 -0
  19. data/hosttag.spec +179 -0
  20. data/lib/hosttag.rb +402 -0
  21. data/lib/hosttag/server.rb +35 -0
  22. data/test/data_hosttag/a/centos +0 -0
  23. data/test/data_hosttag/a/centos5 +0 -0
  24. data/test/data_hosttag/a/centos5-x86_64 +0 -0
  25. data/test/data_hosttag/a/public +0 -0
  26. data/test/data_hosttag/g/SKIP +0 -0
  27. data/test/data_hosttag/g/centos +0 -0
  28. data/test/data_hosttag/g/centos4 +0 -0
  29. data/test/data_hosttag/g/centos4-i386 +0 -0
  30. data/test/data_hosttag/h/SKIP +0 -0
  31. data/test/data_hosttag/h/centos +0 -0
  32. data/test/data_hosttag/h/centos4 +0 -0
  33. data/test/data_hosttag/h/centos4-x86_64 +0 -0
  34. data/test/data_hosttag/h/public +0 -0
  35. data/test/data_hosttag/m/centos +0 -0
  36. data/test/data_hosttag/m/centos4 +0 -0
  37. data/test/data_hosttag/m/centos4-x86_64 +0 -0
  38. data/test/data_hosttag/m/public +0 -0
  39. data/test/data_hosttag/m/vps +0 -0
  40. data/test/data_hosttag/n/centos +0 -0
  41. data/test/data_hosttag/n/centos5 +0 -0
  42. data/test/data_hosttag/n/centos5-i386 +0 -0
  43. data/test/data_hosttag/n/laptop +0 -0
  44. data/test/test_hosttag_bin.rb +70 -0
  45. data/test/test_hosttag_lib.rb +119 -0
  46. data/test/test_htset_bin.rb +174 -0
  47. data/test/test_htset_lib.rb +183 -0
  48. data/test/ts_all.rb +4 -0
  49. metadata +132 -0
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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