hosttag 0.12

Sign up to get free protection for your applications and to get access to all the features.
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