From b87d2f4e68281062df1913440ca5753ae63314a9 Mon Sep 17 00:00:00 2001 From: Dmitry Burmistrov Date: Tue, 17 Sep 2013 21:49:23 +0400 Subject: [PATCH] Updated mcollective.init according to OSCI-658 --- COPYING | 202 + README | 6 + Rakefile | 241 + bin/mc-call-agent | 54 + bin/mco | 37 + bin/mcollectived | 49 + debian/changelog | 5 + debian/compat | 1 + debian/control | 42 + debian/copyright | 29 + debian/mcollective-client.install | 6 + debian/mcollective-common.install | 11 + debian/mcollective-doc.install | 1 + debian/mcollective.init | 85 + debian/mcollective.install | 5 + debian/patches/00list | 1 + debian/patches/conffile.dpatch | 115 + debian/patches/initlsb.dpatch | 38 + debian/patches/makefile.dpatch | 49 + debian/patches/pluginsdir.dpatch | 32 + debian/rules | 18 + doc/mcollective/COPYING | 202 + doc/mcollective/README | 6 + etc/client.cfg.dist | 22 + etc/data-help.erb | 28 + etc/discovery-help.erb | 12 + etc/facts.yaml.dist | 2 + etc/metadata-help.erb | 18 + etc/msg-help.erb | 11 + etc/rpc-help.erb | 44 + etc/server.cfg.dist | 23 + etc/ssl/PLACEHOLDER | 0 etc/ssl/clients/PLACEHOLDER | 0 ext/Makefile | 44 + ext/action_helpers/perl/.gitignore | 3 + ext/action_helpers/perl/Makefile.PL | 9 + .../perl/lib/MCollective/Action.pm | 158 + ext/action_helpers/perl/t/basic.t | 30 + ext/action_helpers/php/README.markdown | 38 + ext/action_helpers/php/mcollective_action.php | 65 + .../python/kwilczynski/README.markdown | 46 + ext/action_helpers/python/kwilczynski/echo.py | 16 + .../python/kwilczynski/mcollective_action.py | 112 + .../python/romke/README.markdown | 38 + .../python/romke/mcollectiveah.py | 69 + ext/action_helpers/python/romke/test.py | 50 + ext/activemq/apache-activemq.spec | 206 + ext/activemq/examples/multi-broker/README | 12 + .../multi-broker/broker1-activemq.xml | 133 + .../multi-broker/broker2-activemq.xml | 73 + .../multi-broker/broker3-activemq.xml | 73 + ext/activemq/examples/single-broker/README | 5 + .../examples/single-broker/activemq.xml | 72 + ext/activemq/wlcg-patch.tgz | Bin 0 -> 9890 bytes ext/bash/mco_completion.sh | 57 + ext/debian/compat | 1 + ext/debian/control | 42 + ext/debian/copyright | 29 + ext/debian/mcollective-client.install | 6 + ext/debian/mcollective-common.install | 11 + ext/debian/mcollective-doc.install | 1 + ext/debian/mcollective.init | 85 + ext/debian/mcollective.install | 5 + ext/debian/patches/00list | 1 + ext/debian/patches/conffile.dpatch | 115 + ext/debian/patches/initlsb.dpatch | 38 + ext/debian/patches/makefile.dpatch | 49 + ext/debian/patches/pluginsdir.dpatch | 32 + ext/debian/rules | 18 + ext/help-templates/README | 1 + ext/help-templates/rpc-help-markdown.erb | 49 + ext/mc-irb | 252 + ext/mc-rpc-restserver.rb | 34 + ext/openbsd/README | 4 + ext/openbsd/port-files/mcollective/Makefile | 28 + ext/openbsd/port-files/mcollective/distinfo | 5 + .../patches/patch-etc_server_cfg_dist | 10 + .../mcollective/patches/patch-ext_Makefile | 67 + ext/openbsd/port-files/mcollective/pkg/DESCR | 2 + .../port-files/mcollective/pkg/MESSAGE | 3 + ext/openbsd/port-files/mcollective/pkg/PLIST | 600 +++ .../mcollective/pkg/mcollectived.rc | 10 + ext/osx/README | 15 + ext/osx/bldmacpkg | 133 + ext/perl/mc-find-hosts.pl | 80 + ext/redhat/mcollective.init | 139 + ext/redhat/mcollective.spec | 130 + ext/solaris/README | 33 + ext/solaris/build | 68 + ext/solaris/cswmcollectived.xml | 100 + ext/solaris/depend | 3 + ext/solaris/mcollective.init | 96 + ext/solaris/pkginfo | 6 + ext/solaris/postinstall | 6 + ext/solaris/postremove | 3 + ext/solaris/preremove | 2 + ext/solaris/prototype.head | 4 + ext/solaris/setversion | 9 + ext/solaris11/Makefile | 46 + ext/solaris11/README.md | 104 + ext/stompclient | 156 + ext/vim/_.snippets | 10 + ext/vim/mcollective_ddl.snippets | 89 + ext/windows/README.md | 34 + ext/windows/environment.bat | 16 + ext/windows/mco.bat | 7 + ext/windows/register_service.bat | 7 + ext/windows/service_manager.rb | 94 + ext/windows/unregister_service.bat | 7 + ext/zsh/_mco | 94 + lib/mcollective.rb | 67 + lib/mcollective/agent.rb | 5 + lib/mcollective/agents.rb | 149 + lib/mcollective/aggregate.rb | 85 + lib/mcollective/aggregate/base.rb | 40 + lib/mcollective/aggregate/result.rb | 9 + lib/mcollective/aggregate/result/base.rb | 25 + .../aggregate/result/collection_result.rb | 19 + .../aggregate/result/numeric_result.rb | 13 + lib/mcollective/application.rb | 374 ++ lib/mcollective/applications.rb | 134 + lib/mcollective/cache.rb | 149 + lib/mcollective/client.rb | 219 + lib/mcollective/config.rb | 223 + lib/mcollective/connector.rb | 18 + lib/mcollective/connector/base.rb | 24 + lib/mcollective/data.rb | 91 + lib/mcollective/data/base.rb | 67 + lib/mcollective/data/result.rb | 40 + lib/mcollective/ddl.rb | 124 + lib/mcollective/ddl/agentddl.rb | 208 + lib/mcollective/ddl/base.rb | 223 + lib/mcollective/ddl/dataddl.rb | 56 + lib/mcollective/ddl/discoveryddl.rb | 52 + lib/mcollective/ddl/validatorddl.rb | 6 + lib/mcollective/discovery.rb | 143 + lib/mcollective/exception.rb | 40 + lib/mcollective/facts.rb | 39 + lib/mcollective/facts/base.rb | 86 + lib/mcollective/generators.rb | 7 + lib/mcollective/generators/agent_generator.rb | 51 + lib/mcollective/generators/base.rb | 46 + lib/mcollective/generators/data_generator.rb | 51 + .../generators/templates/action_snippet.erb | 13 + .../templates/data_input_snippet.erb | 7 + lib/mcollective/generators/templates/ddl.erb | 8 + .../generators/templates/plugin.erb | 7 + lib/mcollective/locales/en.yml | 321 ++ lib/mcollective/log.rb | 168 + lib/mcollective/logger.rb | 5 + lib/mcollective/logger/base.rb | 77 + lib/mcollective/logger/console_logger.rb | 59 + lib/mcollective/logger/file_logger.rb | 46 + lib/mcollective/logger/syslog_logger.rb | 51 + lib/mcollective/matcher.rb | 183 + lib/mcollective/matcher/parser.rb | 128 + lib/mcollective/matcher/scanner.rb | 201 + lib/mcollective/message.rb | 242 + lib/mcollective/monkey_patches.rb | 121 + lib/mcollective/optionparser.rb | 181 + lib/mcollective/pluginmanager.rb | 180 + lib/mcollective/pluginpackager.rb | 68 + .../pluginpackager/agent_definition.rb | 94 + .../pluginpackager/standard_definition.rb | 69 + lib/mcollective/registration.rb | 16 + lib/mcollective/registration/base.rb | 77 + lib/mcollective/rpc.rb | 182 + lib/mcollective/rpc/actionrunner.rb | 142 + lib/mcollective/rpc/agent.rb | 375 ++ lib/mcollective/rpc/audit.rb | 38 + lib/mcollective/rpc/client.rb | 958 ++++ lib/mcollective/rpc/helpers.rb | 306 ++ lib/mcollective/rpc/progress.rb | 63 + lib/mcollective/rpc/reply.rb | 85 + lib/mcollective/rpc/request.rb | 62 + lib/mcollective/rpc/result.rb | 45 + lib/mcollective/rpc/stats.rb | 256 + lib/mcollective/runner.rb | 137 + lib/mcollective/runnerstats.rb | 90 + lib/mcollective/security.rb | 26 + lib/mcollective/security/base.rb | 244 + lib/mcollective/shell.rb | 87 + lib/mcollective/ssl.rb | 280 ++ lib/mcollective/translatable.rb | 24 + lib/mcollective/unix_daemon.rb | 37 + lib/mcollective/util.rb | 466 ++ lib/mcollective/validator.rb | 80 + lib/mcollective/vendor.rb | 41 + lib/mcollective/vendor/i18n/.gitignore | 8 + lib/mcollective/vendor/i18n/.travis.yml | 7 + lib/mcollective/vendor/i18n/CHANGELOG.textile | 152 + lib/mcollective/vendor/i18n/MIT-LICENSE | 20 + lib/mcollective/vendor/i18n/README.textile | 105 + lib/mcollective/vendor/i18n/Rakefile | 27 + lib/mcollective/vendor/i18n/lib/i18n.rb | 332 ++ .../vendor/i18n/lib/i18n/backend.rb | 18 + .../vendor/i18n/lib/i18n/backend/base.rb | 181 + .../vendor/i18n/lib/i18n/backend/cache.rb | 96 + .../vendor/i18n/lib/i18n/backend/cascade.rb | 54 + .../vendor/i18n/lib/i18n/backend/chain.rb | 78 + .../vendor/i18n/lib/i18n/backend/fallbacks.rb | 65 + .../vendor/i18n/lib/i18n/backend/flatten.rb | 113 + .../vendor/i18n/lib/i18n/backend/gettext.rb | 72 + .../i18n/backend/interpolation_compiler.rb | 121 + .../vendor/i18n/lib/i18n/backend/key_value.rb | 101 + .../vendor/i18n/lib/i18n/backend/memoize.rb | 46 + .../vendor/i18n/lib/i18n/backend/metadata.rb | 65 + .../i18n/lib/i18n/backend/pluralization.rb | 53 + .../vendor/i18n/lib/i18n/backend/simple.rb | 87 + .../i18n/lib/i18n/backend/transliterator.rb | 98 + .../vendor/i18n/lib/i18n/config.rb | 86 + .../vendor/i18n/lib/i18n/core_ext/hash.rb | 29 + .../i18n/core_ext/kernel/surpress_warnings.rb | 9 + .../lib/i18n/core_ext/string/interpolate.rb | 105 + .../vendor/i18n/lib/i18n/exceptions.rb | 106 + .../vendor/i18n/lib/i18n/gettext.rb | 25 + .../vendor/i18n/lib/i18n/gettext/helpers.rb | 64 + .../vendor/i18n/lib/i18n/gettext/po_parser.rb | 329 ++ .../vendor/i18n/lib/i18n/interpolate/ruby.rb | 31 + .../vendor/i18n/lib/i18n/locale.rb | 6 + .../vendor/i18n/lib/i18n/locale/fallbacks.rb | 96 + .../vendor/i18n/lib/i18n/locale/tag.rb | 28 + .../i18n/lib/i18n/locale/tag/parents.rb | 22 + .../i18n/lib/i18n/locale/tag/rfc4646.rb | 74 + .../vendor/i18n/lib/i18n/locale/tag/simple.rb | 39 + lib/mcollective/vendor/i18n/lib/i18n/tests.rb | 12 + .../vendor/i18n/lib/i18n/tests/basics.rb | 54 + .../vendor/i18n/lib/i18n/tests/defaults.rb | 40 + .../i18n/lib/i18n/tests/interpolation.rb | 133 + .../vendor/i18n/lib/i18n/tests/link.rb | 56 + .../i18n/lib/i18n/tests/localization.rb | 19 + .../i18n/lib/i18n/tests/localization/date.rb | 84 + .../lib/i18n/tests/localization/date_time.rb | 77 + .../i18n/lib/i18n/tests/localization/procs.rb | 116 + .../i18n/lib/i18n/tests/localization/time.rb | 76 + .../vendor/i18n/lib/i18n/tests/lookup.rb | 74 + .../i18n/lib/i18n/tests/pluralization.rb | 35 + .../vendor/i18n/lib/i18n/tests/procs.rb | 55 + .../vendor/i18n/lib/i18n/version.rb | 3 + lib/mcollective/vendor/json/.gitignore | 8 + lib/mcollective/vendor/json/.travis.yml | 15 + lib/mcollective/vendor/json/CHANGES | 212 + lib/mcollective/vendor/json/COPYING | 58 + .../vendor/json/COPYING-json-jruby | 57 + lib/mcollective/vendor/json/GPL | 340 ++ lib/mcollective/vendor/json/Gemfile | 11 + .../vendor/json/README-json-jruby.markdown | 33 + lib/mcollective/vendor/json/README.rdoc | 358 ++ lib/mcollective/vendor/json/Rakefile | 416 ++ lib/mcollective/vendor/json/TODO | 1 + lib/mcollective/vendor/json/VERSION | 1 + lib/mcollective/vendor/json/bin/edit_json.rb | 9 + .../vendor/json/bin/prettify_json.rb | 48 + lib/mcollective/vendor/json/data/example.json | 1 + lib/mcollective/vendor/json/data/index.html | 38 + lib/mcollective/vendor/json/data/prototype.js | 4184 +++++++++++++++++ lib/mcollective/vendor/json/diagrams/.keep | 0 .../json/ext/json/ext/generator/extconf.rb | 20 + .../json/ext/json/ext/generator/generator.c | 1459 ++++++ .../json/ext/json/ext/generator/generator.h | 200 + .../json/ext/json/ext/parser/extconf.rb | 16 + .../vendor/json/ext/json/ext/parser/parser.c | 2190 +++++++++ .../vendor/json/ext/json/ext/parser/parser.h | 82 + .../vendor/json/ext/json/ext/parser/parser.rl | 913 ++++ lib/mcollective/vendor/json/install.rb | 26 + .../vendor/json/java/lib/bytelist-1.0.6.jar | Bin 0 -> 10493 bytes .../vendor/json/java/lib/jcodings.jar | Bin 0 -> 242327 bytes .../java/src/json/ext/ByteListTranscoder.java | 167 + .../json/java/src/json/ext/Generator.java | 437 ++ .../java/src/json/ext/GeneratorMethods.java | 232 + .../java/src/json/ext/GeneratorService.java | 43 + .../java/src/json/ext/GeneratorState.java | 501 ++ .../json/java/src/json/ext/OptionsReader.java | 119 + .../vendor/json/java/src/json/ext/Parser.java | 2585 ++++++++++ .../vendor/json/java/src/json/ext/Parser.rl | 913 ++++ .../json/java/src/json/ext/ParserService.java | 35 + .../json/java/src/json/ext/RuntimeInfo.java | 121 + .../json/java/src/json/ext/StringDecoder.java | 166 + .../json/java/src/json/ext/StringEncoder.java | 106 + .../vendor/json/java/src/json/ext/Utils.java | 89 + lib/mcollective/vendor/json/json-java.gemspec | 22 + lib/mcollective/vendor/json/json.gemspec | 41 + lib/mcollective/vendor/json/json_pure.gemspec | 46 + lib/mcollective/vendor/json/lib/json.rb | 62 + .../vendor/json/lib/json/Array.xpm | 21 + .../vendor/json/lib/json/FalseClass.xpm | 21 + lib/mcollective/vendor/json/lib/json/Hash.xpm | 21 + lib/mcollective/vendor/json/lib/json/Key.xpm | 73 + .../vendor/json/lib/json/NilClass.xpm | 21 + .../vendor/json/lib/json/Numeric.xpm | 28 + .../vendor/json/lib/json/String.xpm | 96 + .../vendor/json/lib/json/TrueClass.xpm | 21 + .../vendor/json/lib/json/add/complex.rb | 22 + .../vendor/json/lib/json/add/core.rb | 246 + .../vendor/json/lib/json/add/rational.rb | 22 + .../vendor/json/lib/json/common.rb | 442 ++ .../vendor/json/lib/json/editor.rb | 1369 ++++++ lib/mcollective/vendor/json/lib/json/ext.rb | 15 + .../vendor/json/lib/json/ext/.keep | 0 lib/mcollective/vendor/json/lib/json/json.xpm | 1499 ++++++ lib/mcollective/vendor/json/lib/json/pure.rb | 15 + .../vendor/json/lib/json/pure/generator.rb | 457 ++ .../vendor/json/lib/json/pure/parser.rb | 354 ++ .../vendor/json/lib/json/version.rb | 8 + .../vendor/json/tests/fixtures/fail1.json | 1 + .../vendor/json/tests/fixtures/fail10.json | 1 + .../vendor/json/tests/fixtures/fail11.json | 1 + .../vendor/json/tests/fixtures/fail12.json | 1 + .../vendor/json/tests/fixtures/fail13.json | 1 + .../vendor/json/tests/fixtures/fail14.json | 1 + .../vendor/json/tests/fixtures/fail18.json | 1 + .../vendor/json/tests/fixtures/fail19.json | 1 + .../vendor/json/tests/fixtures/fail2.json | 1 + .../vendor/json/tests/fixtures/fail20.json | 1 + .../vendor/json/tests/fixtures/fail21.json | 1 + .../vendor/json/tests/fixtures/fail22.json | 1 + .../vendor/json/tests/fixtures/fail23.json | 1 + .../vendor/json/tests/fixtures/fail24.json | 1 + .../vendor/json/tests/fixtures/fail25.json | 1 + .../vendor/json/tests/fixtures/fail27.json | 2 + .../vendor/json/tests/fixtures/fail28.json | 2 + .../vendor/json/tests/fixtures/fail3.json | 1 + .../vendor/json/tests/fixtures/fail4.json | 1 + .../vendor/json/tests/fixtures/fail5.json | 1 + .../vendor/json/tests/fixtures/fail6.json | 1 + .../vendor/json/tests/fixtures/fail7.json | 1 + .../vendor/json/tests/fixtures/fail8.json | 1 + .../vendor/json/tests/fixtures/fail9.json | 1 + .../vendor/json/tests/fixtures/pass1.json | 56 + .../vendor/json/tests/fixtures/pass15.json | 1 + .../vendor/json/tests/fixtures/pass16.json | 1 + .../vendor/json/tests/fixtures/pass17.json | 1 + .../vendor/json/tests/fixtures/pass2.json | 1 + .../vendor/json/tests/fixtures/pass26.json | 1 + .../vendor/json/tests/fixtures/pass3.json | 6 + .../vendor/json/tests/setup_variant.rb | 11 + .../vendor/json/tests/test_json.rb | 480 ++ .../vendor/json/tests/test_json_addition.rb | 182 + .../vendor/json/tests/test_json_encoding.rb | 65 + .../vendor/json/tests/test_json_fixtures.rb | 35 + .../vendor/json/tests/test_json_generate.rb | 213 + .../json/tests/test_json_string_matching.rb | 39 + .../vendor/json/tests/test_json_unicode.rb | 72 + lib/mcollective/vendor/json/tools/fuzz.rb | 139 + lib/mcollective/vendor/json/tools/server.rb | 61 + lib/mcollective/vendor/load_i18n.rb | 1 + lib/mcollective/vendor/load_json.rb | 1 + lib/mcollective/vendor/load_systemu.rb | 1 + lib/mcollective/vendor/require_vendored.rb | 3 + lib/mcollective/vendor/systemu/LICENSE | 3 + lib/mcollective/vendor/systemu/README | 170 + lib/mcollective/vendor/systemu/README.erb | 37 + lib/mcollective/vendor/systemu/Rakefile | 374 ++ lib/mcollective/vendor/systemu/lib/systemu.rb | 360 ++ lib/mcollective/vendor/systemu/samples/a.rb | 11 + lib/mcollective/vendor/systemu/samples/b.rb | 12 + lib/mcollective/vendor/systemu/samples/c.rb | 10 + lib/mcollective/vendor/systemu/samples/d.rb | 11 + lib/mcollective/vendor/systemu/samples/e.rb | 9 + lib/mcollective/vendor/systemu/samples/f.rb | 18 + .../vendor/systemu/systemu.gemspec | 45 + lib/mcollective/windows_daemon.rb | 25 + mcollective.init | 85 + plugins/mcollective/agent/discovery.rb | 39 + plugins/mcollective/agent/rpcutil.ddl | 204 + plugins/mcollective/agent/rpcutil.rb | 99 + plugins/mcollective/aggregate/average.rb | 29 + plugins/mcollective/aggregate/sum.rb | 18 + plugins/mcollective/aggregate/summary.rb | 53 + plugins/mcollective/application/completion.rb | 104 + plugins/mcollective/application/doc.rb | 25 + plugins/mcollective/application/facts.rb | 53 + plugins/mcollective/application/find.rb | 21 + plugins/mcollective/application/help.rb | 28 + plugins/mcollective/application/inventory.rb | 340 ++ plugins/mcollective/application/ping.rb | 78 + plugins/mcollective/application/plugin.rb | 335 ++ plugins/mcollective/application/rpc.rb | 114 + plugins/mcollective/audit/logfile.rb | 25 + plugins/mcollective/connector/activemq.rb | 402 ++ plugins/mcollective/connector/rabbitmq.rb | 306 ++ plugins/mcollective/data/agent_data.ddl | 22 + plugins/mcollective/data/agent_data.rb | 17 + plugins/mcollective/data/fstat_data.ddl | 89 + plugins/mcollective/data/fstat_data.rb | 56 + plugins/mcollective/discovery/flatfile.ddl | 11 + plugins/mcollective/discovery/flatfile.rb | 40 + plugins/mcollective/discovery/mc.ddl | 11 + plugins/mcollective/discovery/mc.rb | 30 + plugins/mcollective/facts/yaml_facts.rb | 61 + .../pluginpackager/debpackage_packager.rb | 147 + .../pluginpackager/ospackage_packager.rb | 59 + .../pluginpackager/rpmpackage_packager.rb | 97 + .../templates/debian/Makefile.erb | 7 + .../templates/debian/changelog.erb | 5 + .../templates/debian/compat.erb | 1 + .../templates/debian/control.erb | 27 + .../templates/debian/copyright.erb | 8 + .../pluginpackager/templates/debian/rules.erb | 6 + .../templates/redhat/rpm_spec.erb | 61 + plugins/mcollective/registration/agentlist.rb | 10 + plugins/mcollective/security/aes_security.rb | 329 ++ plugins/mcollective/security/psk.rb | 117 + plugins/mcollective/security/ssl.rb | 328 ++ .../mcollective/validator/array_validator.ddl | 7 + .../mcollective/validator/array_validator.rb | 9 + .../validator/ipv4address_validator.ddl | 7 + .../validator/ipv4address_validator.rb | 16 + .../validator/ipv6address_validator.ddl | 7 + .../validator/ipv6address_validator.rb | 16 + .../validator/length_validator.ddl | 7 + .../mcollective/validator/length_validator.rb | 11 + .../mcollective/validator/regex_validator.ddl | 7 + .../mcollective/validator/regex_validator.rb | 9 + .../validator/shellsafe_validator.ddl | 7 + .../validator/shellsafe_validator.rb | 13 + .../validator/typecheck_validator.ddl | 7 + .../validator/typecheck_validator.rb | 28 + spec/Rakefile | 16 + spec/fixtures/application/test.rb | 7 + spec/fixtures/test-cert.pem | 15 + spec/fixtures/test-private.pem | 15 + spec/fixtures/test-public.pem | 6 + spec/fixtures/util/1.in | 10 + spec/fixtures/util/1.out | 10 + spec/fixtures/util/2.in | 1 + spec/fixtures/util/2.out | 1 + spec/fixtures/util/3.in | 1 + spec/fixtures/util/3.out | 2 + spec/fixtures/util/4.in | 5 + spec/fixtures/util/4.out | 9 + spec/matchers/exception_matchers.rb | 75 + .../instance_variable_defined.rb | 7 + spec/spec.opts | 1 + spec/spec_helper.rb | 33 + spec/unit/agents_spec.rb | 295 ++ spec/unit/aggregate/base_spec.rb | 57 + spec/unit/aggregate/result/base_spec.rb | 28 + .../result/collection_result_spec.rb | 18 + .../aggregate/result/numeric_result_spec.rb | 22 + spec/unit/aggregate_spec.rb | 170 + spec/unit/application_spec.rb | 653 +++ spec/unit/applications_spec.rb | 155 + spec/unit/array_spec.rb | 30 + spec/unit/cache_spec.rb | 161 + spec/unit/client_spec.rb | 78 + spec/unit/config_spec.rb | 168 + spec/unit/data/base_spec.rb | 90 + spec/unit/data/result_spec.rb | 72 + spec/unit/data_spec.rb | 163 + spec/unit/ddl/agentddl_spec.rb | 249 + spec/unit/ddl/base_spec.rb | 403 ++ spec/unit/ddl/dataddl_spec.rb | 65 + spec/unit/ddl/discoveryddl_spec.rb | 58 + spec/unit/ddl_spec.rb | 84 + spec/unit/discovery_spec.rb | 196 + spec/unit/facts/base_spec.rb | 118 + spec/unit/facts_spec.rb | 39 + spec/unit/generators/agent_generator_spec.rb | 72 + spec/unit/generators/base_spec.rb | 83 + spec/unit/generators/data_generator_spec.rb | 37 + spec/unit/generators/snippets/agent_ddl | 19 + spec/unit/generators/snippets/data_ddl | 20 + spec/unit/log_spec.rb | 144 + spec/unit/logger/base_spec.rb | 118 + spec/unit/logger/console_logger_spec.rb | 67 + spec/unit/logger/syslog_logger_spec.rb | 79 + spec/unit/matcher/parser_spec.rb | 123 + spec/unit/matcher/scanner_spec.rb | 174 + spec/unit/matcher_spec.rb | 260 + spec/unit/message_spec.rb | 423 ++ spec/unit/optionparser_spec.rb | 113 + spec/unit/pluginmanager_spec.rb | 173 + .../pluginpackager/agent_definition_spec.rb | 173 + .../standard_definition_spec.rb | 100 + spec/unit/pluginpackager_spec.rb | 131 + .../mcollective/aggregate/average_spec.rb | 45 + .../plugins/mcollective/aggregate/sum_spec.rb | 31 + .../mcollective/aggregate/summary_spec.rb | 55 + .../mcollective/connector/activemq_spec.rb | 534 +++ .../mcollective/connector/rabbitmq_spec.rb | 479 ++ .../mcollective/data/agent_data_spec.rb | 43 + .../mcollective/data/fstat_data_spec.rb | 135 + .../mcollective/discovery/flatfile_spec.rb | 48 + .../plugins/mcollective/discovery/mc_spec.rb | 40 + .../packagers/debpackage_packager_spec.rb | 311 ++ .../mcollective/packagers/ospackage_spec.rb | 57 + .../packagers/rpmpackage_packager_spec.rb | 169 + .../plugins/mcollective/security/psk_spec.rb | 156 + .../validator/array_validator_spec.rb | 19 + .../validator/ipv4address_validator_spec.rb | 19 + .../validator/ipv6address_validator_spec.rb | 19 + .../validator/length_validator_spec.rb | 19 + .../validator/regex_validator_spec.rb | 19 + .../validator/shellsafe_validator_spec.rb | 21 + .../validator/typecheck_validator_spec.rb | 23 + spec/unit/registration/base_spec.rb | 77 + spec/unit/rpc/actionrunner_spec.rb | 213 + spec/unit/rpc/agent_spec.rb | 260 + spec/unit/rpc/client_spec.rb | 861 ++++ spec/unit/rpc/helpers_spec.rb | 55 + spec/unit/rpc/reply_spec.rb | 173 + spec/unit/rpc/request_spec.rb | 136 + spec/unit/rpc/result_spec.rb | 73 + spec/unit/rpc/stats_spec.rb | 324 ++ spec/unit/rpc_spec.rb | 16 + spec/unit/runnerstats_spec.rb | 40 + spec/unit/security/base_spec.rb | 279 ++ spec/unit/shell_spec.rb | 144 + spec/unit/ssl_spec.rb | 262 ++ spec/unit/string_spec.rb | 15 + spec/unit/symbol_spec.rb | 11 + spec/unit/unix_daemon_spec.rb | 41 + spec/unit/util_spec.rb | 494 ++ spec/unit/validator_spec.rb | 67 + spec/unit/vendor_spec.rb | 34 + spec/unit/windows_daemon_spec.rb | 43 + spec/windows_spec.opts | 1 + website/_includes/main_menu.html | 19 + website/blueprint/ie.css | 35 + .../blueprint/plugins/buttons/icons/cross.png | Bin 0 -> 655 bytes .../blueprint/plugins/buttons/icons/key.png | Bin 0 -> 455 bytes .../blueprint/plugins/buttons/icons/tick.png | Bin 0 -> 537 bytes website/blueprint/plugins/buttons/readme.txt | 32 + website/blueprint/plugins/buttons/screen.css | 97 + .../blueprint/plugins/fancy-type/readme.txt | 14 + .../blueprint/plugins/fancy-type/screen.css | 71 + .../plugins/link-icons/icons/doc.png | Bin 0 -> 777 bytes .../plugins/link-icons/icons/email.png | Bin 0 -> 641 bytes .../plugins/link-icons/icons/external.png | Bin 0 -> 46848 bytes .../plugins/link-icons/icons/feed.png | Bin 0 -> 691 bytes .../blueprint/plugins/link-icons/icons/im.png | Bin 0 -> 741 bytes .../plugins/link-icons/icons/pdf.png | Bin 0 -> 591 bytes .../plugins/link-icons/icons/visited.png | Bin 0 -> 46990 bytes .../plugins/link-icons/icons/xls.png | Bin 0 -> 663 bytes .../blueprint/plugins/link-icons/readme.txt | 18 + .../blueprint/plugins/link-icons/screen.css | 40 + website/blueprint/plugins/rtl/readme.txt | 10 + website/blueprint/plugins/rtl/screen.css | 110 + website/blueprint/print.css | 29 + website/blueprint/screen.css | 258 + website/blueprint/src/forms.css | 65 + website/blueprint/src/grid.css | 280 ++ website/blueprint/src/grid.png | Bin 0 -> 195 bytes website/blueprint/src/ie.css | 76 + website/blueprint/src/print.css | 85 + website/blueprint/src/reset.css | 45 + website/blueprint/src/typography.css | 106 + website/changelog.md | 526 +++ website/ec2demo.md | 45 + website/images/activemq-multi-locations.png | Bin 0 -> 77356 bytes website/images/mcollective-aaa.png | Bin 0 -> 69828 bytes website/images/mcollective/li.png | Bin 0 -> 149 bytes website/images/message-flow-diagram.png | Bin 0 -> 91289 bytes website/images/subcollectives-collectives.png | Bin 0 -> 99788 bytes website/images/subcollectives-impact.png | Bin 0 -> 57834 bytes .../subcollectives-multiple-middleware.png | Bin 0 -> 48589 bytes website/index.md | 109 + website/messages/PLMC1.md | 18 + website/messages/PLMC10.md | 26 + website/messages/PLMC11.md | 22 + website/messages/PLMC12.md | 24 + website/messages/PLMC13.md | 20 + website/messages/PLMC14.md | 27 + website/messages/PLMC15.md | 20 + website/messages/PLMC16.md | 22 + website/messages/PLMC17.md | 22 + website/messages/PLMC18.md | 20 + website/messages/PLMC19.md | 22 + website/messages/PLMC2.md | 18 + website/messages/PLMC20.md | 20 + website/messages/PLMC21.md | 20 + website/messages/PLMC22.md | 30 + website/messages/PLMC23.md | 18 + website/messages/PLMC24.md | 20 + website/messages/PLMC25.md | 24 + website/messages/PLMC26.md | 20 + website/messages/PLMC27.md | 22 + website/messages/PLMC28.md | 22 + website/messages/PLMC29.md | 20 + website/messages/PLMC3.md | 22 + website/messages/PLMC30.md | 18 + website/messages/PLMC31.md | 25 + website/messages/PLMC32.md | 18 + website/messages/PLMC33.md | 20 + website/messages/PLMC34.md | 24 + website/messages/PLMC35.md | 20 + website/messages/PLMC36.md | 27 + website/messages/PLMC37.md | 20 + website/messages/PLMC38.md | 24 + website/messages/PLMC39.md | 20 + website/messages/PLMC4.md | 20 + website/messages/PLMC5.md | 20 + website/messages/PLMC6.md | 24 + website/messages/PLMC7.md | 20 + website/messages/PLMC8.md | 20 + website/messages/PLMC9.md | 24 + .../reference/basic/basic_agent_and_client.md | 262 ++ website/reference/basic/basic_cli_usage.md | 500 ++ website/reference/basic/configuration.md | 119 + website/reference/basic/daemon.md | 156 + website/reference/basic/gettingstarted.md | 283 ++ .../reference/basic/gettingstarted_debian.md | 312 ++ .../reference/basic/gettingstarted_redhat.md | 308 ++ website/reference/basic/messageflow.md | 28 + website/reference/basic/messageformat.md | 160 + website/reference/basic/subcollectives.md | 212 + website/reference/development/ec2_demo.md | 104 + website/reference/development/releasetasks.md | 47 + website/reference/index.md | 56 + .../integration/activemq_clusters.md | 117 + .../integration/activemq_security.md | 92 + website/reference/integration/activemq_ssl.md | 287 ++ website/reference/integration/chef.md | 75 + website/reference/integration/puppet.md | 74 + website/reference/plugins/aggregate.md | 369 ++ website/reference/plugins/application.md | 308 ++ .../reference/plugins/connector_activemq.md | 135 + .../reference/plugins/connector_rabbitmq.md | 85 + website/reference/plugins/connector_stomp.md | 86 + website/reference/plugins/data.md | 265 ++ website/reference/plugins/ddl.md | 274 ++ website/reference/plugins/discovery.md | 158 + website/reference/plugins/facts.md | 82 + website/reference/plugins/registration.md | 68 + website/reference/plugins/rpcutil.md | 116 + website/reference/plugins/security_aes.md | 278 ++ website/reference/plugins/security_ssl.md | 124 + website/reference/plugins/validator.md | 127 + website/reference/ui/filters.md | 92 + website/reference/ui/nodereports.md | 175 + website/releasenotes.md | 2670 +++++++++++ website/screencasts.md | 120 + website/security.md | 192 + website/simplerpc/agents.md | 526 +++ website/simplerpc/auditing.md | 69 + website/simplerpc/authorization.md | 61 + website/simplerpc/clients.md | 578 +++ website/simplerpc/index.md | 102 + website/simplerpc/messageformat.md | 92 + website/terminology.md | 101 + 641 files changed, 71479 insertions(+) create mode 100644 COPYING create mode 100644 README create mode 100644 Rakefile create mode 100755 bin/mc-call-agent create mode 100755 bin/mco create mode 100755 bin/mcollectived create mode 100644 debian/changelog create mode 100644 debian/compat create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/mcollective-client.install create mode 100644 debian/mcollective-common.install create mode 100644 debian/mcollective-doc.install create mode 100755 debian/mcollective.init create mode 100644 debian/mcollective.install create mode 100644 debian/patches/00list create mode 100755 debian/patches/conffile.dpatch create mode 100755 debian/patches/initlsb.dpatch create mode 100755 debian/patches/makefile.dpatch create mode 100755 debian/patches/pluginsdir.dpatch create mode 100755 debian/rules create mode 100644 doc/mcollective/COPYING create mode 100644 doc/mcollective/README create mode 100644 etc/client.cfg.dist create mode 100644 etc/data-help.erb create mode 100644 etc/discovery-help.erb create mode 100644 etc/facts.yaml.dist create mode 100644 etc/metadata-help.erb create mode 100644 etc/msg-help.erb create mode 100644 etc/rpc-help.erb create mode 100644 etc/server.cfg.dist create mode 100644 etc/ssl/PLACEHOLDER create mode 100644 etc/ssl/clients/PLACEHOLDER create mode 100644 ext/Makefile create mode 100644 ext/action_helpers/perl/.gitignore create mode 100644 ext/action_helpers/perl/Makefile.PL create mode 100644 ext/action_helpers/perl/lib/MCollective/Action.pm create mode 100644 ext/action_helpers/perl/t/basic.t create mode 100644 ext/action_helpers/php/README.markdown create mode 100644 ext/action_helpers/php/mcollective_action.php create mode 100644 ext/action_helpers/python/kwilczynski/README.markdown create mode 100644 ext/action_helpers/python/kwilczynski/echo.py create mode 100644 ext/action_helpers/python/kwilczynski/mcollective_action.py create mode 100644 ext/action_helpers/python/romke/README.markdown create mode 100644 ext/action_helpers/python/romke/mcollectiveah.py create mode 100644 ext/action_helpers/python/romke/test.py create mode 100644 ext/activemq/apache-activemq.spec create mode 100644 ext/activemq/examples/multi-broker/README create mode 100755 ext/activemq/examples/multi-broker/broker1-activemq.xml create mode 100755 ext/activemq/examples/multi-broker/broker2-activemq.xml create mode 100755 ext/activemq/examples/multi-broker/broker3-activemq.xml create mode 100644 ext/activemq/examples/single-broker/README create mode 100644 ext/activemq/examples/single-broker/activemq.xml create mode 100644 ext/activemq/wlcg-patch.tgz create mode 100644 ext/bash/mco_completion.sh create mode 100644 ext/debian/compat create mode 100644 ext/debian/control create mode 100644 ext/debian/copyright create mode 100644 ext/debian/mcollective-client.install create mode 100644 ext/debian/mcollective-common.install create mode 100644 ext/debian/mcollective-doc.install create mode 100755 ext/debian/mcollective.init create mode 100644 ext/debian/mcollective.install create mode 100644 ext/debian/patches/00list create mode 100755 ext/debian/patches/conffile.dpatch create mode 100755 ext/debian/patches/initlsb.dpatch create mode 100755 ext/debian/patches/makefile.dpatch create mode 100755 ext/debian/patches/pluginsdir.dpatch create mode 100755 ext/debian/rules create mode 100644 ext/help-templates/README create mode 100644 ext/help-templates/rpc-help-markdown.erb create mode 100755 ext/mc-irb create mode 100755 ext/mc-rpc-restserver.rb create mode 100644 ext/openbsd/README create mode 100644 ext/openbsd/port-files/mcollective/Makefile create mode 100644 ext/openbsd/port-files/mcollective/distinfo create mode 100644 ext/openbsd/port-files/mcollective/patches/patch-etc_server_cfg_dist create mode 100644 ext/openbsd/port-files/mcollective/patches/patch-ext_Makefile create mode 100644 ext/openbsd/port-files/mcollective/pkg/DESCR create mode 100644 ext/openbsd/port-files/mcollective/pkg/MESSAGE create mode 100644 ext/openbsd/port-files/mcollective/pkg/PLIST create mode 100644 ext/openbsd/port-files/mcollective/pkg/mcollectived.rc create mode 100644 ext/osx/README create mode 100644 ext/osx/bldmacpkg create mode 100644 ext/perl/mc-find-hosts.pl create mode 100755 ext/redhat/mcollective.init create mode 100644 ext/redhat/mcollective.spec create mode 100644 ext/solaris/README create mode 100755 ext/solaris/build create mode 100644 ext/solaris/cswmcollectived.xml create mode 100644 ext/solaris/depend create mode 100755 ext/solaris/mcollective.init create mode 100644 ext/solaris/pkginfo create mode 100644 ext/solaris/postinstall create mode 100644 ext/solaris/postremove create mode 100644 ext/solaris/preremove create mode 100644 ext/solaris/prototype.head create mode 100755 ext/solaris/setversion create mode 100644 ext/solaris11/Makefile create mode 100644 ext/solaris11/README.md create mode 100755 ext/stompclient create mode 100644 ext/vim/_.snippets create mode 100644 ext/vim/mcollective_ddl.snippets create mode 100644 ext/windows/README.md create mode 100644 ext/windows/environment.bat create mode 100644 ext/windows/mco.bat create mode 100644 ext/windows/register_service.bat create mode 100644 ext/windows/service_manager.rb create mode 100644 ext/windows/unregister_service.bat create mode 100644 ext/zsh/_mco create mode 100644 lib/mcollective.rb create mode 100644 lib/mcollective/agent.rb create mode 100644 lib/mcollective/agents.rb create mode 100644 lib/mcollective/aggregate.rb create mode 100644 lib/mcollective/aggregate/base.rb create mode 100644 lib/mcollective/aggregate/result.rb create mode 100644 lib/mcollective/aggregate/result/base.rb create mode 100644 lib/mcollective/aggregate/result/collection_result.rb create mode 100644 lib/mcollective/aggregate/result/numeric_result.rb create mode 100644 lib/mcollective/application.rb create mode 100644 lib/mcollective/applications.rb create mode 100644 lib/mcollective/cache.rb create mode 100644 lib/mcollective/client.rb create mode 100644 lib/mcollective/config.rb create mode 100644 lib/mcollective/connector.rb create mode 100644 lib/mcollective/connector/base.rb create mode 100644 lib/mcollective/data.rb create mode 100644 lib/mcollective/data/base.rb create mode 100644 lib/mcollective/data/result.rb create mode 100644 lib/mcollective/ddl.rb create mode 100644 lib/mcollective/ddl/agentddl.rb create mode 100644 lib/mcollective/ddl/base.rb create mode 100644 lib/mcollective/ddl/dataddl.rb create mode 100644 lib/mcollective/ddl/discoveryddl.rb create mode 100644 lib/mcollective/ddl/validatorddl.rb create mode 100644 lib/mcollective/discovery.rb create mode 100644 lib/mcollective/exception.rb create mode 100644 lib/mcollective/facts.rb create mode 100644 lib/mcollective/facts/base.rb create mode 100644 lib/mcollective/generators.rb create mode 100644 lib/mcollective/generators/agent_generator.rb create mode 100644 lib/mcollective/generators/base.rb create mode 100644 lib/mcollective/generators/data_generator.rb create mode 100644 lib/mcollective/generators/templates/action_snippet.erb create mode 100644 lib/mcollective/generators/templates/data_input_snippet.erb create mode 100644 lib/mcollective/generators/templates/ddl.erb create mode 100644 lib/mcollective/generators/templates/plugin.erb create mode 100644 lib/mcollective/locales/en.yml create mode 100644 lib/mcollective/log.rb create mode 100644 lib/mcollective/logger.rb create mode 100644 lib/mcollective/logger/base.rb create mode 100644 lib/mcollective/logger/console_logger.rb create mode 100644 lib/mcollective/logger/file_logger.rb create mode 100644 lib/mcollective/logger/syslog_logger.rb create mode 100644 lib/mcollective/matcher.rb create mode 100644 lib/mcollective/matcher/parser.rb create mode 100644 lib/mcollective/matcher/scanner.rb create mode 100644 lib/mcollective/message.rb create mode 100644 lib/mcollective/monkey_patches.rb create mode 100644 lib/mcollective/optionparser.rb create mode 100644 lib/mcollective/pluginmanager.rb create mode 100644 lib/mcollective/pluginpackager.rb create mode 100644 lib/mcollective/pluginpackager/agent_definition.rb create mode 100644 lib/mcollective/pluginpackager/standard_definition.rb create mode 100644 lib/mcollective/registration.rb create mode 100644 lib/mcollective/registration/base.rb create mode 100644 lib/mcollective/rpc.rb create mode 100644 lib/mcollective/rpc/actionrunner.rb create mode 100644 lib/mcollective/rpc/agent.rb create mode 100644 lib/mcollective/rpc/audit.rb create mode 100644 lib/mcollective/rpc/client.rb create mode 100644 lib/mcollective/rpc/helpers.rb create mode 100644 lib/mcollective/rpc/progress.rb create mode 100644 lib/mcollective/rpc/reply.rb create mode 100644 lib/mcollective/rpc/request.rb create mode 100644 lib/mcollective/rpc/result.rb create mode 100644 lib/mcollective/rpc/stats.rb create mode 100644 lib/mcollective/runner.rb create mode 100644 lib/mcollective/runnerstats.rb create mode 100644 lib/mcollective/security.rb create mode 100644 lib/mcollective/security/base.rb create mode 100644 lib/mcollective/shell.rb create mode 100644 lib/mcollective/ssl.rb create mode 100644 lib/mcollective/translatable.rb create mode 100644 lib/mcollective/unix_daemon.rb create mode 100644 lib/mcollective/util.rb create mode 100644 lib/mcollective/validator.rb create mode 100644 lib/mcollective/vendor.rb create mode 100644 lib/mcollective/vendor/i18n/.gitignore create mode 100644 lib/mcollective/vendor/i18n/.travis.yml create mode 100644 lib/mcollective/vendor/i18n/CHANGELOG.textile create mode 100755 lib/mcollective/vendor/i18n/MIT-LICENSE create mode 100644 lib/mcollective/vendor/i18n/README.textile create mode 100644 lib/mcollective/vendor/i18n/Rakefile create mode 100755 lib/mcollective/vendor/i18n/lib/i18n.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/base.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/cache.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/cascade.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/chain.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/fallbacks.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/flatten.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/gettext.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/interpolation_compiler.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/key_value.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/memoize.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/metadata.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/pluralization.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/simple.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/backend/transliterator.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/config.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/core_ext/hash.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/core_ext/kernel/surpress_warnings.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/core_ext/string/interpolate.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/exceptions.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/gettext.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/gettext/helpers.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/gettext/po_parser.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/interpolate/ruby.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/locale.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/locale/fallbacks.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/locale/tag.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/locale/tag/parents.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/locale/tag/rfc4646.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/locale/tag/simple.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/basics.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/defaults.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/interpolation.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/link.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/localization.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/localization/date.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/localization/date_time.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/localization/procs.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/localization/time.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/lookup.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/pluralization.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/tests/procs.rb create mode 100644 lib/mcollective/vendor/i18n/lib/i18n/version.rb create mode 100644 lib/mcollective/vendor/json/.gitignore create mode 100644 lib/mcollective/vendor/json/.travis.yml create mode 100644 lib/mcollective/vendor/json/CHANGES create mode 100644 lib/mcollective/vendor/json/COPYING create mode 100644 lib/mcollective/vendor/json/COPYING-json-jruby create mode 100644 lib/mcollective/vendor/json/GPL create mode 100644 lib/mcollective/vendor/json/Gemfile create mode 100644 lib/mcollective/vendor/json/README-json-jruby.markdown create mode 100644 lib/mcollective/vendor/json/README.rdoc create mode 100644 lib/mcollective/vendor/json/Rakefile create mode 100644 lib/mcollective/vendor/json/TODO create mode 100644 lib/mcollective/vendor/json/VERSION create mode 100755 lib/mcollective/vendor/json/bin/edit_json.rb create mode 100755 lib/mcollective/vendor/json/bin/prettify_json.rb create mode 100644 lib/mcollective/vendor/json/data/example.json create mode 100644 lib/mcollective/vendor/json/data/index.html create mode 100644 lib/mcollective/vendor/json/data/prototype.js create mode 100644 lib/mcollective/vendor/json/diagrams/.keep create mode 100644 lib/mcollective/vendor/json/ext/json/ext/generator/extconf.rb create mode 100644 lib/mcollective/vendor/json/ext/json/ext/generator/generator.c create mode 100644 lib/mcollective/vendor/json/ext/json/ext/generator/generator.h create mode 100644 lib/mcollective/vendor/json/ext/json/ext/parser/extconf.rb create mode 100644 lib/mcollective/vendor/json/ext/json/ext/parser/parser.c create mode 100644 lib/mcollective/vendor/json/ext/json/ext/parser/parser.h create mode 100644 lib/mcollective/vendor/json/ext/json/ext/parser/parser.rl create mode 100755 lib/mcollective/vendor/json/install.rb create mode 100644 lib/mcollective/vendor/json/java/lib/bytelist-1.0.6.jar create mode 100644 lib/mcollective/vendor/json/java/lib/jcodings.jar create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/ByteListTranscoder.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/Generator.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/GeneratorMethods.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/GeneratorService.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/GeneratorState.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/OptionsReader.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/Parser.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/Parser.rl create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/ParserService.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/RuntimeInfo.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/StringDecoder.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/StringEncoder.java create mode 100644 lib/mcollective/vendor/json/java/src/json/ext/Utils.java create mode 100644 lib/mcollective/vendor/json/json-java.gemspec create mode 100644 lib/mcollective/vendor/json/json.gemspec create mode 100644 lib/mcollective/vendor/json/json_pure.gemspec create mode 100644 lib/mcollective/vendor/json/lib/json.rb create mode 100644 lib/mcollective/vendor/json/lib/json/Array.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/FalseClass.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/Hash.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/Key.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/NilClass.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/Numeric.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/String.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/TrueClass.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/add/complex.rb create mode 100644 lib/mcollective/vendor/json/lib/json/add/core.rb create mode 100644 lib/mcollective/vendor/json/lib/json/add/rational.rb create mode 100644 lib/mcollective/vendor/json/lib/json/common.rb create mode 100644 lib/mcollective/vendor/json/lib/json/editor.rb create mode 100644 lib/mcollective/vendor/json/lib/json/ext.rb create mode 100644 lib/mcollective/vendor/json/lib/json/ext/.keep create mode 100644 lib/mcollective/vendor/json/lib/json/json.xpm create mode 100644 lib/mcollective/vendor/json/lib/json/pure.rb create mode 100644 lib/mcollective/vendor/json/lib/json/pure/generator.rb create mode 100644 lib/mcollective/vendor/json/lib/json/pure/parser.rb create mode 100644 lib/mcollective/vendor/json/lib/json/version.rb create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail1.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail10.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail11.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail12.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail13.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail14.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail18.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail19.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail2.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail20.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail21.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail22.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail23.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail24.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail25.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail27.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail28.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail3.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail4.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail5.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail6.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail7.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail8.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/fail9.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/pass1.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/pass15.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/pass16.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/pass17.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/pass2.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/pass26.json create mode 100644 lib/mcollective/vendor/json/tests/fixtures/pass3.json create mode 100644 lib/mcollective/vendor/json/tests/setup_variant.rb create mode 100755 lib/mcollective/vendor/json/tests/test_json.rb create mode 100755 lib/mcollective/vendor/json/tests/test_json_addition.rb create mode 100644 lib/mcollective/vendor/json/tests/test_json_encoding.rb create mode 100755 lib/mcollective/vendor/json/tests/test_json_fixtures.rb create mode 100755 lib/mcollective/vendor/json/tests/test_json_generate.rb create mode 100644 lib/mcollective/vendor/json/tests/test_json_string_matching.rb create mode 100755 lib/mcollective/vendor/json/tests/test_json_unicode.rb create mode 100755 lib/mcollective/vendor/json/tools/fuzz.rb create mode 100755 lib/mcollective/vendor/json/tools/server.rb create mode 100644 lib/mcollective/vendor/load_i18n.rb create mode 100644 lib/mcollective/vendor/load_json.rb create mode 100644 lib/mcollective/vendor/load_systemu.rb create mode 100644 lib/mcollective/vendor/require_vendored.rb create mode 100644 lib/mcollective/vendor/systemu/LICENSE create mode 100644 lib/mcollective/vendor/systemu/README create mode 100644 lib/mcollective/vendor/systemu/README.erb create mode 100644 lib/mcollective/vendor/systemu/Rakefile create mode 100644 lib/mcollective/vendor/systemu/lib/systemu.rb create mode 100644 lib/mcollective/vendor/systemu/samples/a.rb create mode 100644 lib/mcollective/vendor/systemu/samples/b.rb create mode 100644 lib/mcollective/vendor/systemu/samples/c.rb create mode 100644 lib/mcollective/vendor/systemu/samples/d.rb create mode 100644 lib/mcollective/vendor/systemu/samples/e.rb create mode 100644 lib/mcollective/vendor/systemu/samples/f.rb create mode 100644 lib/mcollective/vendor/systemu/systemu.gemspec create mode 100644 lib/mcollective/windows_daemon.rb create mode 100644 mcollective.init create mode 100644 plugins/mcollective/agent/discovery.rb create mode 100644 plugins/mcollective/agent/rpcutil.ddl create mode 100644 plugins/mcollective/agent/rpcutil.rb create mode 100644 plugins/mcollective/aggregate/average.rb create mode 100644 plugins/mcollective/aggregate/sum.rb create mode 100644 plugins/mcollective/aggregate/summary.rb create mode 100644 plugins/mcollective/application/completion.rb create mode 100644 plugins/mcollective/application/doc.rb create mode 100644 plugins/mcollective/application/facts.rb create mode 100644 plugins/mcollective/application/find.rb create mode 100644 plugins/mcollective/application/help.rb create mode 100644 plugins/mcollective/application/inventory.rb create mode 100644 plugins/mcollective/application/ping.rb create mode 100644 plugins/mcollective/application/plugin.rb create mode 100644 plugins/mcollective/application/rpc.rb create mode 100644 plugins/mcollective/audit/logfile.rb create mode 100644 plugins/mcollective/connector/activemq.rb create mode 100644 plugins/mcollective/connector/rabbitmq.rb create mode 100644 plugins/mcollective/data/agent_data.ddl create mode 100644 plugins/mcollective/data/agent_data.rb create mode 100644 plugins/mcollective/data/fstat_data.ddl create mode 100644 plugins/mcollective/data/fstat_data.rb create mode 100644 plugins/mcollective/discovery/flatfile.ddl create mode 100644 plugins/mcollective/discovery/flatfile.rb create mode 100644 plugins/mcollective/discovery/mc.ddl create mode 100644 plugins/mcollective/discovery/mc.rb create mode 100644 plugins/mcollective/facts/yaml_facts.rb create mode 100644 plugins/mcollective/pluginpackager/debpackage_packager.rb create mode 100644 plugins/mcollective/pluginpackager/ospackage_packager.rb create mode 100644 plugins/mcollective/pluginpackager/rpmpackage_packager.rb create mode 100644 plugins/mcollective/pluginpackager/templates/debian/Makefile.erb create mode 100644 plugins/mcollective/pluginpackager/templates/debian/changelog.erb create mode 100644 plugins/mcollective/pluginpackager/templates/debian/compat.erb create mode 100644 plugins/mcollective/pluginpackager/templates/debian/control.erb create mode 100644 plugins/mcollective/pluginpackager/templates/debian/copyright.erb create mode 100644 plugins/mcollective/pluginpackager/templates/debian/rules.erb create mode 100644 plugins/mcollective/pluginpackager/templates/redhat/rpm_spec.erb create mode 100644 plugins/mcollective/registration/agentlist.rb create mode 100644 plugins/mcollective/security/aes_security.rb create mode 100644 plugins/mcollective/security/psk.rb create mode 100644 plugins/mcollective/security/ssl.rb create mode 100644 plugins/mcollective/validator/array_validator.ddl create mode 100644 plugins/mcollective/validator/array_validator.rb create mode 100644 plugins/mcollective/validator/ipv4address_validator.ddl create mode 100644 plugins/mcollective/validator/ipv4address_validator.rb create mode 100644 plugins/mcollective/validator/ipv6address_validator.ddl create mode 100644 plugins/mcollective/validator/ipv6address_validator.rb create mode 100644 plugins/mcollective/validator/length_validator.ddl create mode 100644 plugins/mcollective/validator/length_validator.rb create mode 100644 plugins/mcollective/validator/regex_validator.ddl create mode 100644 plugins/mcollective/validator/regex_validator.rb create mode 100644 plugins/mcollective/validator/shellsafe_validator.ddl create mode 100644 plugins/mcollective/validator/shellsafe_validator.rb create mode 100644 plugins/mcollective/validator/typecheck_validator.ddl create mode 100644 plugins/mcollective/validator/typecheck_validator.rb create mode 100644 spec/Rakefile create mode 100644 spec/fixtures/application/test.rb create mode 100644 spec/fixtures/test-cert.pem create mode 100644 spec/fixtures/test-private.pem create mode 100644 spec/fixtures/test-public.pem create mode 100644 spec/fixtures/util/1.in create mode 100644 spec/fixtures/util/1.out create mode 100644 spec/fixtures/util/2.in create mode 100644 spec/fixtures/util/2.out create mode 100644 spec/fixtures/util/3.in create mode 100644 spec/fixtures/util/3.out create mode 100644 spec/fixtures/util/4.in create mode 100644 spec/fixtures/util/4.out create mode 100644 spec/matchers/exception_matchers.rb create mode 100644 spec/monkey_patches/instance_variable_defined.rb create mode 100644 spec/spec.opts create mode 100644 spec/spec_helper.rb create mode 100755 spec/unit/agents_spec.rb create mode 100644 spec/unit/aggregate/base_spec.rb create mode 100644 spec/unit/aggregate/result/base_spec.rb create mode 100644 spec/unit/aggregate/result/collection_result_spec.rb create mode 100644 spec/unit/aggregate/result/numeric_result_spec.rb create mode 100644 spec/unit/aggregate_spec.rb create mode 100755 spec/unit/application_spec.rb create mode 100755 spec/unit/applications_spec.rb create mode 100755 spec/unit/array_spec.rb create mode 100644 spec/unit/cache_spec.rb create mode 100644 spec/unit/client_spec.rb create mode 100755 spec/unit/config_spec.rb create mode 100644 spec/unit/data/base_spec.rb create mode 100644 spec/unit/data/result_spec.rb create mode 100644 spec/unit/data_spec.rb create mode 100644 spec/unit/ddl/agentddl_spec.rb create mode 100644 spec/unit/ddl/base_spec.rb create mode 100644 spec/unit/ddl/dataddl_spec.rb create mode 100644 spec/unit/ddl/discoveryddl_spec.rb create mode 100644 spec/unit/ddl_spec.rb create mode 100644 spec/unit/discovery_spec.rb create mode 100755 spec/unit/facts/base_spec.rb create mode 100755 spec/unit/facts_spec.rb create mode 100644 spec/unit/generators/agent_generator_spec.rb create mode 100644 spec/unit/generators/base_spec.rb create mode 100644 spec/unit/generators/data_generator_spec.rb create mode 100644 spec/unit/generators/snippets/agent_ddl create mode 100644 spec/unit/generators/snippets/data_ddl create mode 100755 spec/unit/log_spec.rb create mode 100755 spec/unit/logger/base_spec.rb create mode 100644 spec/unit/logger/console_logger_spec.rb create mode 100644 spec/unit/logger/syslog_logger_spec.rb create mode 100755 spec/unit/matcher/parser_spec.rb create mode 100755 spec/unit/matcher/scanner_spec.rb create mode 100644 spec/unit/matcher_spec.rb create mode 100755 spec/unit/message_spec.rb create mode 100755 spec/unit/optionparser_spec.rb create mode 100755 spec/unit/pluginmanager_spec.rb create mode 100644 spec/unit/pluginpackager/agent_definition_spec.rb create mode 100644 spec/unit/pluginpackager/standard_definition_spec.rb create mode 100644 spec/unit/pluginpackager_spec.rb create mode 100644 spec/unit/plugins/mcollective/aggregate/average_spec.rb create mode 100644 spec/unit/plugins/mcollective/aggregate/sum_spec.rb create mode 100644 spec/unit/plugins/mcollective/aggregate/summary_spec.rb create mode 100644 spec/unit/plugins/mcollective/connector/activemq_spec.rb create mode 100644 spec/unit/plugins/mcollective/connector/rabbitmq_spec.rb create mode 100644 spec/unit/plugins/mcollective/data/agent_data_spec.rb create mode 100644 spec/unit/plugins/mcollective/data/fstat_data_spec.rb create mode 100644 spec/unit/plugins/mcollective/discovery/flatfile_spec.rb create mode 100644 spec/unit/plugins/mcollective/discovery/mc_spec.rb create mode 100644 spec/unit/plugins/mcollective/packagers/debpackage_packager_spec.rb create mode 100644 spec/unit/plugins/mcollective/packagers/ospackage_spec.rb create mode 100644 spec/unit/plugins/mcollective/packagers/rpmpackage_packager_spec.rb create mode 100755 spec/unit/plugins/mcollective/security/psk_spec.rb create mode 100644 spec/unit/plugins/mcollective/validator/array_validator_spec.rb create mode 100644 spec/unit/plugins/mcollective/validator/ipv4address_validator_spec.rb create mode 100644 spec/unit/plugins/mcollective/validator/ipv6address_validator_spec.rb create mode 100644 spec/unit/plugins/mcollective/validator/length_validator_spec.rb create mode 100644 spec/unit/plugins/mcollective/validator/regex_validator_spec.rb create mode 100644 spec/unit/plugins/mcollective/validator/shellsafe_validator_spec.rb create mode 100644 spec/unit/plugins/mcollective/validator/typecheck_validator_spec.rb create mode 100755 spec/unit/registration/base_spec.rb create mode 100755 spec/unit/rpc/actionrunner_spec.rb create mode 100755 spec/unit/rpc/agent_spec.rb create mode 100644 spec/unit/rpc/client_spec.rb create mode 100755 spec/unit/rpc/helpers_spec.rb create mode 100755 spec/unit/rpc/reply_spec.rb create mode 100755 spec/unit/rpc/request_spec.rb create mode 100755 spec/unit/rpc/result_spec.rb create mode 100755 spec/unit/rpc/stats_spec.rb create mode 100755 spec/unit/rpc_spec.rb create mode 100755 spec/unit/runnerstats_spec.rb create mode 100755 spec/unit/security/base_spec.rb create mode 100755 spec/unit/shell_spec.rb create mode 100755 spec/unit/ssl_spec.rb create mode 100755 spec/unit/string_spec.rb create mode 100755 spec/unit/symbol_spec.rb create mode 100755 spec/unit/unix_daemon_spec.rb create mode 100755 spec/unit/util_spec.rb create mode 100644 spec/unit/validator_spec.rb create mode 100755 spec/unit/vendor_spec.rb create mode 100755 spec/unit/windows_daemon_spec.rb create mode 100644 spec/windows_spec.opts create mode 100644 website/_includes/main_menu.html create mode 100644 website/blueprint/ie.css create mode 100755 website/blueprint/plugins/buttons/icons/cross.png create mode 100755 website/blueprint/plugins/buttons/icons/key.png create mode 100755 website/blueprint/plugins/buttons/icons/tick.png create mode 100644 website/blueprint/plugins/buttons/readme.txt create mode 100644 website/blueprint/plugins/buttons/screen.css create mode 100644 website/blueprint/plugins/fancy-type/readme.txt create mode 100644 website/blueprint/plugins/fancy-type/screen.css create mode 100644 website/blueprint/plugins/link-icons/icons/doc.png create mode 100644 website/blueprint/plugins/link-icons/icons/email.png create mode 100644 website/blueprint/plugins/link-icons/icons/external.png create mode 100644 website/blueprint/plugins/link-icons/icons/feed.png create mode 100644 website/blueprint/plugins/link-icons/icons/im.png create mode 100644 website/blueprint/plugins/link-icons/icons/pdf.png create mode 100644 website/blueprint/plugins/link-icons/icons/visited.png create mode 100644 website/blueprint/plugins/link-icons/icons/xls.png create mode 100644 website/blueprint/plugins/link-icons/readme.txt create mode 100644 website/blueprint/plugins/link-icons/screen.css create mode 100644 website/blueprint/plugins/rtl/readme.txt create mode 100644 website/blueprint/plugins/rtl/screen.css create mode 100644 website/blueprint/print.css create mode 100644 website/blueprint/screen.css create mode 100644 website/blueprint/src/forms.css create mode 100755 website/blueprint/src/grid.css create mode 100644 website/blueprint/src/grid.png create mode 100644 website/blueprint/src/ie.css create mode 100755 website/blueprint/src/print.css create mode 100755 website/blueprint/src/reset.css create mode 100644 website/blueprint/src/typography.css create mode 100644 website/changelog.md create mode 100644 website/ec2demo.md create mode 100644 website/images/activemq-multi-locations.png create mode 100644 website/images/mcollective-aaa.png create mode 100644 website/images/mcollective/li.png create mode 100644 website/images/message-flow-diagram.png create mode 100644 website/images/subcollectives-collectives.png create mode 100644 website/images/subcollectives-impact.png create mode 100644 website/images/subcollectives-multiple-middleware.png create mode 100644 website/index.md create mode 100644 website/messages/PLMC1.md create mode 100644 website/messages/PLMC10.md create mode 100644 website/messages/PLMC11.md create mode 100644 website/messages/PLMC12.md create mode 100644 website/messages/PLMC13.md create mode 100644 website/messages/PLMC14.md create mode 100644 website/messages/PLMC15.md create mode 100644 website/messages/PLMC16.md create mode 100644 website/messages/PLMC17.md create mode 100644 website/messages/PLMC18.md create mode 100644 website/messages/PLMC19.md create mode 100644 website/messages/PLMC2.md create mode 100644 website/messages/PLMC20.md create mode 100644 website/messages/PLMC21.md create mode 100644 website/messages/PLMC22.md create mode 100644 website/messages/PLMC23.md create mode 100644 website/messages/PLMC24.md create mode 100644 website/messages/PLMC25.md create mode 100644 website/messages/PLMC26.md create mode 100644 website/messages/PLMC27.md create mode 100644 website/messages/PLMC28.md create mode 100644 website/messages/PLMC29.md create mode 100644 website/messages/PLMC3.md create mode 100644 website/messages/PLMC30.md create mode 100644 website/messages/PLMC31.md create mode 100644 website/messages/PLMC32.md create mode 100644 website/messages/PLMC33.md create mode 100644 website/messages/PLMC34.md create mode 100644 website/messages/PLMC35.md create mode 100644 website/messages/PLMC36.md create mode 100644 website/messages/PLMC37.md create mode 100644 website/messages/PLMC38.md create mode 100644 website/messages/PLMC39.md create mode 100644 website/messages/PLMC4.md create mode 100644 website/messages/PLMC5.md create mode 100644 website/messages/PLMC6.md create mode 100644 website/messages/PLMC7.md create mode 100644 website/messages/PLMC8.md create mode 100644 website/messages/PLMC9.md create mode 100644 website/reference/basic/basic_agent_and_client.md create mode 100644 website/reference/basic/basic_cli_usage.md create mode 100644 website/reference/basic/configuration.md create mode 100644 website/reference/basic/daemon.md create mode 100644 website/reference/basic/gettingstarted.md create mode 100644 website/reference/basic/gettingstarted_debian.md create mode 100644 website/reference/basic/gettingstarted_redhat.md create mode 100644 website/reference/basic/messageflow.md create mode 100644 website/reference/basic/messageformat.md create mode 100644 website/reference/basic/subcollectives.md create mode 100644 website/reference/development/ec2_demo.md create mode 100644 website/reference/development/releasetasks.md create mode 100644 website/reference/index.md create mode 100644 website/reference/integration/activemq_clusters.md create mode 100644 website/reference/integration/activemq_security.md create mode 100644 website/reference/integration/activemq_ssl.md create mode 100644 website/reference/integration/chef.md create mode 100644 website/reference/integration/puppet.md create mode 100644 website/reference/plugins/aggregate.md create mode 100644 website/reference/plugins/application.md create mode 100644 website/reference/plugins/connector_activemq.md create mode 100644 website/reference/plugins/connector_rabbitmq.md create mode 100644 website/reference/plugins/connector_stomp.md create mode 100644 website/reference/plugins/data.md create mode 100644 website/reference/plugins/ddl.md create mode 100644 website/reference/plugins/discovery.md create mode 100644 website/reference/plugins/facts.md create mode 100644 website/reference/plugins/registration.md create mode 100644 website/reference/plugins/rpcutil.md create mode 100644 website/reference/plugins/security_aes.md create mode 100644 website/reference/plugins/security_ssl.md create mode 100644 website/reference/plugins/validator.md create mode 100644 website/reference/ui/filters.md create mode 100644 website/reference/ui/nodereports.md create mode 100644 website/releasenotes.md create mode 100644 website/screencasts.md create mode 100644 website/security.md create mode 100644 website/simplerpc/agents.md create mode 100644 website/simplerpc/auditing.md create mode 100644 website/simplerpc/authorization.md create mode 100644 website/simplerpc/clients.md create mode 100644 website/simplerpc/index.md create mode 100644 website/simplerpc/messageformat.md create mode 100644 website/terminology.md diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..375d1c8 --- /dev/null +++ b/COPYING @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2010, 2011 Puppet Labs + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README b/README new file mode 100644 index 0000000..8db05de --- /dev/null +++ b/README @@ -0,0 +1,6 @@ +The Marionette Collective +========================= + +The Marionette Collective aka. mcollective is a framework to build server orchestration or parallel job execution systems. + +For full information, wikis, ticketing and downloads please see http://marionette-collective.org/ diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..c1020d0 --- /dev/null +++ b/Rakefile @@ -0,0 +1,241 @@ +# Rakefile to build a project using HUDSON + +begin + require 'rdoc/task' +rescue LoadError + require 'rake/rdoctask' +end + +require 'rake/packagetask' +require 'rake/clean' +require 'find' +require 'rubygems/package_task' + +PROJ_DOC_TITLE = "The Marionette Collective" +PROJ_VERSION = "2.3.1" +PROJ_RELEASE = "2" +PROJ_NAME = "mcollective" +PROJ_RPM_NAMES = [PROJ_NAME] +PROJ_FILES = ["#{PROJ_NAME}.init", "COPYING", "doc", "etc", "lib", "plugins", "ext", "bin"] +PROJ_FILES.concat(Dir.glob("mc-*")) +RDOC_EXCLUDES = ["mcollective/vendor", "spec", "ext", "website", "plugins"] + +ENV["RPM_VERSION"] ? CURRENT_VERSION = ENV["RPM_VERSION"] : CURRENT_VERSION = PROJ_VERSION +ENV["BUILD_NUMBER"] ? CURRENT_RELEASE = ENV["BUILD_NUMBER"] : CURRENT_RELEASE = PROJ_RELEASE +ENV["DEB_DISTRIBUTION"] ? PKG_DEB_DISTRIBUTION = ENV["DEB_DISTRIBUTION"] : PKG_DEB_DISTRIBUTION = "unstable" + +CLEAN.include(["build", "doc"]) + +def announce(msg='') + STDERR.puts "================" + STDERR.puts msg + STDERR.puts "================" +end + +def init + FileUtils.mkdir("build") unless File.exist?("build") +end + +def safe_system *args + raise RuntimeError, "Failed: #{args.join(' ')}" unless system *args +end + +spec = Gem::Specification.new do |s| + s.name = "mcollective-client" + s.version = PROJ_VERSION + s.author = "R.I.Pienaar" + s.email = "rip@puppetlabs.com" + s.homepage = "https://docs.puppetlabs.com/mcollective/" + s.summary = "Client libraries for The Marionette Collective" + s.description = "Client libraries for the mcollective Application Server" + s.files = FileList["{bin,lib}/**/*"].to_a + s.require_path = "lib" + s.test_files = FileList["spec/**/*"].to_a + s.has_rdoc = true + s.executables = "mco" + s.default_executable = "mco" + s.add_dependency "systemu" + s.add_dependency "json" + s.add_dependency "stomp" + s.add_dependency "i18n" + + excluded_files = ["bin/mcollectived", "lib/mcollective/runner.rb", "lib/mcollective/vendor/json", "lib/mcollective/vendor/systemu", "lib/mcollective/vendor/i18n", "lib/mcollective/vendor/load"] + + excluded_files.each do |file| + s.files.delete_if {|f| f.match(/^#{file}/)} + end +end + +Gem::PackageTask.new(spec) do |pkg| + pkg.need_tar = false + pkg.need_zip = false + pkg.package_dir = "build" +end + +desc "Build documentation, tar balls and rpms" +task :default => [:clean, :doc, :package] + +# task for building docs +rd = Rake::RDocTask.new(:doc) { |rdoc| + rdoc.rdoc_dir = 'doc' + rdoc.title = "#{PROJ_DOC_TITLE} version #{CURRENT_VERSION}" + rdoc.options << '--line-numbers' << '--main' << 'MCollective' + + RDOC_EXCLUDES.each do |ext| + rdoc.options << '--exclude' << ext + end +} + +desc "Run spec tests" +task :test do + sh "cd spec;rake" +end + +desc "Create a tarball for this release" +task :package => [:clean, :doc] do + announce "Creating #{PROJ_NAME}-#{CURRENT_VERSION}.tgz" + + FileUtils.mkdir_p("build/#{PROJ_NAME}-#{CURRENT_VERSION}") + safe_system("cp -R #{PROJ_FILES.join(' ')} build/#{PROJ_NAME}-#{CURRENT_VERSION}") + + announce "Setting MCollective.version to #{CURRENT_VERSION}" + safe_system("cd build/#{PROJ_NAME}-#{CURRENT_VERSION}/lib && sed -i -e s/@DEVELOPMENT_VERSION@/#{CURRENT_VERSION}/ mcollective.rb") + + safe_system("cd build && tar --exclude .svn -cvzf #{PROJ_NAME}-#{CURRENT_VERSION}.tgz #{PROJ_NAME}-#{CURRENT_VERSION}") +end + +desc "Creates the website as a tarball" +task :website => [:clean] do + FileUtils.mkdir_p("build/marionette-collective.org/html") + + Dir.chdir("website") do + safe_system("jekyll ../build/marionette-collective.org/html") + end + + unless File.exist?("build/marionette-collective.org/html/index.html") + raise "Failed to build website" + end + + Dir.chdir("build") do + safe_system("tar -cvzf marionette-collective-org-#{Time.now.to_i}.tgz marionette-collective.org") + end +end + +desc "Creates a RPM" +task :rpm => [:clean, :doc, :package] do + announce("Building RPM for #{PROJ_NAME}-#{CURRENT_VERSION}-#{CURRENT_RELEASE}") + + sourcedir = `rpm --eval '%_sourcedir'`.chomp + specsdir = `rpm --eval '%_specdir'`.chomp + srpmsdir = `rpm --eval '%_srcrpmdir'`.chomp + rpmdir = `rpm --eval '%_rpmdir'`.chomp + lsbdistrel = `lsb_release -r -s | cut -d . -f1`.chomp + lsbdistro = `lsb_release -i -s`.chomp + + `which rpmbuild-md5` + rpmcmd = $?.success? ? 'rpmbuild-md5' : 'rpmbuild' + + case lsbdistro + when 'CentOS' + rpmdist = ".el#{lsbdistrel}" + when 'Fedora' + rpmdist = ".fc#{lsbdistrel}" + else + rpmdist = "" + end + + safe_system %{cp build/#{PROJ_NAME}-#{CURRENT_VERSION}.tgz #{sourcedir}} + safe_system %{cat ext/redhat/#{PROJ_NAME}.spec|sed -e s/%{rpm_release}/#{CURRENT_RELEASE}/g | sed -e s/%{version}/#{CURRENT_VERSION}/g > #{specsdir}/#{PROJ_NAME}.spec} + + if ENV['SIGNED'] == '1' + safe_system %{#{rpmcmd} --sign -D 'version #{CURRENT_VERSION}' -D 'rpm_release #{CURRENT_RELEASE}' -D 'dist #{rpmdist}' -D 'use_lsb 0' -ba #{specsdir}/#{PROJ_NAME}.spec} + else + safe_system %{#{rpmcmd} -D 'version #{CURRENT_VERSION}' -D 'rpm_release #{CURRENT_RELEASE}' -D 'dist #{rpmdist}' -D 'use_lsb 0' -ba #{specsdir}/#{PROJ_NAME}.spec} + end + + safe_system %{cp #{srpmsdir}/#{PROJ_NAME}-#{CURRENT_VERSION}-#{CURRENT_RELEASE}#{rpmdist}.src.rpm build/} + + safe_system %{cp #{rpmdir}/*/#{PROJ_NAME}*-#{CURRENT_VERSION}-#{CURRENT_RELEASE}#{rpmdist}.*.rpm build/} +end + +desc "Create the .debs" +task :deb => [:clean, :doc, :package] do + announce("Building debian packages") + + FileUtils.mkdir_p("build/deb") + Dir.chdir("build/deb") do + safe_system %{tar -xzf ../#{PROJ_NAME}-#{CURRENT_VERSION}.tgz} + safe_system %{cp ../#{PROJ_NAME}-#{CURRENT_VERSION}.tgz #{PROJ_NAME}_#{CURRENT_VERSION}.orig.tar.gz} + + Dir.chdir("#{PROJ_NAME}-#{CURRENT_VERSION}") do + safe_system %{cp -R ext/debian .} + safe_system %{cp -R ext/debian/mcollective.init .} + safe_system %{cp -R ext/Makefile .} + + File.open("debian/changelog", "w") do |f| + f.puts("mcollective (#{CURRENT_VERSION}-#{CURRENT_RELEASE}) #{PKG_DEB_DISTRIBUTION}; urgency=low") + f.puts + f.puts(" * Automated release for #{CURRENT_VERSION}-#{CURRENT_RELEASE} by rake deb") + f.puts + f.puts(" See http://marionette-collective.org/releasenotes.html for full details") + f.puts + f.puts(" -- The Marionette Collective #{Time.new.strftime('%a, %d %b %Y %H:%M:%S %z')}") + end + + if ENV['SIGNED'] == '1' + if ENV['SIGNWITH'] + safe_system %{debuild -i -k#{ENV['SIGNWITH']}} + else + safe_system %{debuild -i} + end + else + safe_system %{debuild -i -us -uc} + end + end + + safe_system %{cp *.deb *.dsc *.diff.gz *.orig.tar.gz *.changes ..} + end +end + +desc "Update the website error code reference based on current local" +task :update_msgweb do + mcollective_dir = File.join(File.dirname(__FILE__)) + + $:.insert(0, File.join(mcollective_dir, "lib")) + + require 'mcollective' + + messages = YAML.load_file(File.join(mcollective_dir, "lib", "mcollective", "locales", "en.yml")) + + webdir = File.join(mcollective_dir, "website", "messages") + + I18n.load_path = Dir[File.join(mcollective_dir, "lib", "mcollective", "locales", "*.yml")] + I18n.locale = :en + + messages["en"].keys.each do |msg_code| + md_file = File.join(webdir, "#{msg_code}.md") + + puts "....writing %s" % md_file + + File.open(md_file, "w") do |md| + md.puts "---" + md.puts "layout: default" + md.puts "title: Message detail for %s" % msg_code + md.puts "toc: false" + md.puts "---" + md.puts + md.puts "Detail for Marionette Collective message %s" % msg_code + md.puts "===========================================" + md.puts + md.puts "Example Message" + md.puts "---------------" + md.puts + md.puts " %s" % (MCollective::Util.t("%s.example" % msg_code, :raise => true) rescue MCollective::Util.t("%s.pattern" % msg_code)) + md.puts + md.puts "Additional Information" + md.puts "----------------------" + md.puts + md.puts MCollective::Util.t("%s.expanded" % msg_code, :raise => true) + end + end +end diff --git a/bin/mc-call-agent b/bin/mc-call-agent new file mode 100755 index 0000000..ca285dd --- /dev/null +++ b/bin/mc-call-agent @@ -0,0 +1,54 @@ +#!/usr/bin/env ruby + +require 'mcollective' +require 'pp' + +oparser = MCollective::Optionparser.new({:verbose => true}, "filter") + +options = oparser.parse{|parser, options| + parser.define_head "Call an agent parsing an argument to it" + parser.banner = "Usage: mc-call-agent [options] --agent agent --argument arg" + + parser.on('-a', '--agent AGENT', 'Agent to call') do |v| + options[:agent] = v + end + + parser.on('--arg', '--argument ARGUMENT', 'Argument to pass to agent') do |v| + options[:argument] = v + end +} + +if options[:agent] == nil || options[:argument] == nil + puts("Please use either --agent or --argument") + exit 1 +end + +begin + options[:filter]["agent"] << options[:agent] + + client = MCollective::Client.new(options[:config]) + client.options = options + + c = 0 + + stats = client.discovered_req(options[:argument], options[:agent]) do |resp| + next if resp == nil + + c += 1 + + if options[:verbose] + puts("#{resp[:senderid]}>") + pp resp[:body] + else + puts if c % 4 == 1 + printf("%-30s", resp[:senderid]) + end + end + + client.disconnect +rescue Exception => e + STDERR.puts "Could not call remote agent: #{e}" + exit 1 +end + +client.display_stats(stats) diff --git a/bin/mco b/bin/mco new file mode 100755 index 0000000..dd692fc --- /dev/null +++ b/bin/mco @@ -0,0 +1,37 @@ +#!/usr/bin/env ruby + +require 'mcollective' + +Version = MCollective.version +known_applications = MCollective::Applications.list + +# links from mc-ping to mc will result in ping being run +if $0 =~ /mc\-([a-zA-Z\-_\.]+)$/ + app_name = $1 +else + app_name = ARGV.first + ARGV.delete_at(0) +end + +if known_applications.include?(app_name) + # make sure the various options classes shows the right help etc + $0 = app_name + + MCollective::Applications.run(app_name) +else + puts "The Marionette Collective version #{MCollective.version}" + puts + puts "usage: #{$0} command " + puts + puts "Known commands:" + puts + + known_applications.sort.uniq.in_groups_of(3) do |apps| + puts " %-20s %-20s %-20s" % [apps[0], apps[1], apps[2]] + end + + puts + puts "Type '#{$0} help' for a detailed list of commands and '#{$0} help command'" + puts "to get detailed help for a command" + puts +end diff --git a/bin/mcollectived b/bin/mcollectived new file mode 100755 index 0000000..4f0d1d8 --- /dev/null +++ b/bin/mcollectived @@ -0,0 +1,49 @@ +#!/usr/bin/env ruby + +require 'mcollective' +require 'getoptlong' + +opts = GetoptLong.new( + [ '--help', '-h', GetoptLong::NO_ARGUMENT ], + [ '--config', '-c', GetoptLong::REQUIRED_ARGUMENT], + [ '--pidfile', '-p', GetoptLong::REQUIRED_ARGUMENT] +) + +configfile = "/etc/mcollective/server.cfg" +pid = "" + +opts.each do |opt, arg| + case opt + when '--help' + puts "Usage: mcollectived.rb [--config /path/to/config] [--pidfile /path/to/pid]" + exit + when '--config' + configfile = arg + when '--pidfile' + pid = arg + end +end + +config = MCollective::Config.instance + +config.loadconfig(configfile) unless config.configured + +MCollective::Log.info("The Marionette Collective #{MCollective::VERSION} started logging at #{config.loglevel} level") + +if config.daemonize + MCollective::Log.debug("Starting in the background (#{config.daemonize})") + + if MCollective::Util.windows? + require 'mcollective/windows_daemon' + + MCollective::WindowsDaemon.daemonize_runner + else + require 'mcollective/unix_daemon' + + MCollective::UnixDaemon.daemonize_runner(pid) + end +else + MCollective::Log.debug("Starting in the foreground") + runner = MCollective::Runner.new(configfile) + runner.run +end diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 0000000..9eece1d --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +mcollective (2.3.1-0mira2) precise; urgency=low + + * Initial deb package release + + -- Mirantis Product Mon, 19 Aug 2013 14:42:18 +0400 diff --git a/debian/compat b/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/debian/compat @@ -0,0 +1 @@ +7 diff --git a/debian/control b/debian/control new file mode 100644 index 0000000..7fb71de --- /dev/null +++ b/debian/control @@ -0,0 +1,42 @@ +Source: mcollective +Section: utils +Priority: extra +Maintainer: Riccardo Setti +Build-Depends: debhelper (>= 7), dpatch, cdbs +Standards-Version: 3.8.0 +Homepage: http://marionette-collective.org/ + +Package: mcollective +Architecture: all +Depends: ruby (>= 1.8.1), mcollective-common (= ${source:Version}) +Description: build server orchestration or parallel job execution systems + The Marionette Collective aka. mcollective is a framework + to build server orchestration or parallel job execution systems. + +Package: mcollective-client +Architecture: all +Depends: ruby (>= 1.8.1), mcollective-common (= ${source:Version}) +Description: build server orchestration or parallel job execution systems + The Marionette Collective aka. mcollective is a framework + to build server orchestration or parallel job execution system + +Package: mcollective-common +Replaces: mcollective (<< 2.0.0-1) +Breaks: mcollective (<< 2.0.0-1), mcollective-client (<< 2.0.0-1) +Architecture: all +Depends: ruby (>= 1.8.1) , rubygems +Description: build server orchestration or parallel job execution systems + The Marionette Collective aka. mcollective is a framework + to build server orchestration or parallel job execution systems. + . + Common files for mcollective packages. + +Package: mcollective-doc +Architecture: all +Section: doc +Description: Documentation for mcollective + The Marionette Collective aka. mcollective is a framework + to build server orchestration or parallel job execution systems. + . + Documentation package. + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 0000000..ec55a9a --- /dev/null +++ b/debian/copyright @@ -0,0 +1,29 @@ +This package was debianized by Riccardo Setti on +Mon, 04 Jan 2010 17:09:50 +0000. + +It was downloaded from http://code.google.com/p/mcollective + +Upstream Author: + R.I.Pienaar + +Copyright: + + Copyright 2009 R.I.Pienaar + +License: + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The Debian packaging is (C) 2010, Riccardo Setti and +is licensed under the Apache License v2. + diff --git a/debian/mcollective-client.install b/debian/mcollective-client.install new file mode 100644 index 0000000..31b43d3 --- /dev/null +++ b/debian/mcollective-client.install @@ -0,0 +1,6 @@ +usr/bin/mco usr/bin/ +usr/sbin/mc-* usr/sbin/ +etc/mcollective/client.cfg etc/mcollective +usr/share/mcollective/plugins/mcollective/application usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/aggregate usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/pluginpackager usr/share/mcollective/plugins/mcollective diff --git a/debian/mcollective-common.install b/debian/mcollective-common.install new file mode 100644 index 0000000..058fa88 --- /dev/null +++ b/debian/mcollective-common.install @@ -0,0 +1,11 @@ +usr/lib/ruby/1.8/* usr/lib/ruby/1.8/ +etc/mcollective/*.erb etc/mcollective +usr/share/mcollective/plugins/mcollective/agent usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/audit usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/connector usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/data usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/facts usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/registration usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/security usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/validator usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/discovery usr/share/mcollective/plugins/mcollective diff --git a/debian/mcollective-doc.install b/debian/mcollective-doc.install new file mode 100644 index 0000000..0bd8547 --- /dev/null +++ b/debian/mcollective-doc.install @@ -0,0 +1 @@ +usr/share/doc/mcollective/* usr/share/doc/mcollective-doc/ diff --git a/debian/mcollective.init b/debian/mcollective.init new file mode 100755 index 0000000..f599f4a --- /dev/null +++ b/debian/mcollective.init @@ -0,0 +1,85 @@ +#!/bin/sh +# +# mcollective Application Server for STOMP based agents +# +# +# description: mcollective lets you build powerful Stomp compatible middleware clients in ruby without having to worry too +# much about all the setup and management of a Stomp connection, it also provides stats, logging and so forth +# as a bonus. +# +### BEGIN INIT INFO +# Provides: mcollective +# Required-Start: $remote_fs +# Required-Stop: $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start daemon at boot time +# Description: Enable service provided by mcollective. +### END INIT INFO + +# check permissions + +uid=`id -u` +[ $uid -gt 0 ] && { echo "You need to be root to run file" ; exit 4 ; } + + +# PID directory +pidfile="/var/run/mcollectived.pid" + +name="mcollective" +mcollectived=/usr/sbin/mcollectived +daemonopts="--pid=${pidfile} --config=/etc/mcollective/server.cfg" + + +# Source function library. +. /lib/lsb/init-functions + +# Check that binary exists +if ! [ -f $mcollectived ] +then + echo "mcollectived binary not found" + exit 5 +fi + +# create pid file if it does not exist +[ ! -f ${pidfile} ] && { touch ${pidfile} ; } + +# See how we were called. +case "$1" in + start) + echo "Starting daemon: " $name + # start the program + start-stop-daemon -S -p ${pidfile} --oknodo -q -a ${mcollectived} -- ${daemonopts} + [ $? = 0 ] && { exit 0 ; } || { exit 1 ; } + log_success_msg "mcollective started" + touch $lock + ;; + stop) + echo "Stopping daemon: " $name + start-stop-daemon -K -R 5 -s "TERM" --oknodo -q -p ${pidfile} + [ $? = 0 ] && { exit 0 ; } || { exit 1 ; } + log_success_msg "mcollective stopped" + ;; + restart) + echo "Restarting daemon: " $name + $0 stop + sleep 2 + $0 start + [ $? = 0 ] && { echo "mcollective restarted" ; exit 0 ; } + ;; + condrestart) + if [ -f $lock ]; then + $0 stop + # avoid race + sleep 2 + $0 start + fi + ;; + status) + status_of_proc -p ${pidfile} ${mcollectived} ${name} && exit 0 || exit $? + ;; + *) + echo "Usage: mcollectived {start|stop|restart|condrestart|status}" + exit 2 + ;; +esac diff --git a/debian/mcollective.install b/debian/mcollective.install new file mode 100644 index 0000000..64e7158 --- /dev/null +++ b/debian/mcollective.install @@ -0,0 +1,5 @@ +usr/sbin/mcollectived usr/sbin +etc/mcollective/facts.yaml etc/mcollective +etc/mcollective/server.cfg etc/mcollective +etc/init.d etc/ +etc/mcollective/ssl diff --git a/debian/patches/00list b/debian/patches/00list new file mode 100644 index 0000000..f356934 --- /dev/null +++ b/debian/patches/00list @@ -0,0 +1 @@ +pluginsdir.dpatch diff --git a/debian/patches/conffile.dpatch b/debian/patches/conffile.dpatch new file mode 100755 index 0000000..07ba476 --- /dev/null +++ b/debian/patches/conffile.dpatch @@ -0,0 +1,115 @@ +#! /bin/sh /usr/share/dpatch/dpatch-run +## conffile.dpatch by +## +## All lines beginning with `## DP:' are a description of the patch. +## DP: fix plugins dir + +@DPATCH@ +diff -urNad mcollective-1.1.4~/etc/client.cfg.dist mcollective-1.1.4/etc/client.cfg.dist +--- mcollective-1.1.4~/etc/client.cfg.dist 2011-04-06 14:13:08.829462165 -0700 ++++ mcollective-1.1.4/etc/client.cfg.dist 2011-04-06 14:12:53.129384114 -0700 +@@ -1,7 +1,7 @@ + topicprefix = /topic/ + main_collective = mcollective + collectives = mcollective +-libdir = /usr/libexec/mcollective ++libdir = /usr/share/mcollective/plugins + logger_type = console + loglevel = warn + +diff -urNad mcollective-1.1.4~/etc/server.cfg.dist mcollective-1.1.4/etc/server.cfg.dist +--- mcollective-1.1.4~/etc/server.cfg.dist 2011-04-06 14:12:30.889527230 -0700 ++++ mcollective-1.1.4/etc/server.cfg.dist 2011-04-06 14:12:23.779407065 -0700 +@@ -1,7 +1,7 @@ + topicprefix = /topic/ + main_collective = mcollective + collectives = mcollective +-libdir = /usr/libexec/mcollective ++libdir = /usr/share/mcollective/plugins + logfile = /var/log/mcollective.log + loglevel = info + daemonize = 1 + diff --git a/debian/rules b/debian/rules new file mode 100755 index 0000000..2551380 --- /dev/null +++ b/debian/rules @@ -0,0 +1,18 @@ +#!/usr/bin/make -f + +DEB_MAKE_CLEAN_TARGET := +DEB_MAKE_INSTALL_TARGET := install DESTDIR=$(CURDIR)/debian/tmp + +include /usr/share/cdbs/1/rules/debhelper.mk +include /usr/share/cdbs/1/rules/dpatch.mk +include /usr/share/cdbs/1/class/makefile.mk +DEB_MAKE_INVOKE = $(DEB_MAKE_ENVVARS) make -f ext/Makefile -C $(DEB_BUILDDIR) + +install/mcollective:: + mv $(CURDIR)/debian/tmp/etc/mcollective/server.cfg.dist $(CURDIR)/debian/tmp/etc/mcollective/server.cfg + mv $(CURDIR)/debian/tmp/etc/mcollective/client.cfg.dist $(CURDIR)/debian/tmp/etc/mcollective/client.cfg + mv $(CURDIR)/debian/tmp/etc/mcollective/facts.yaml.dist $(CURDIR)/debian/tmp/etc/mcollective/facts.yaml +# dh_installinit -pmcollective -o + +binary-fixup/mcollective:: + chmod 640 $(CURDIR)/debian/mcollective/etc/mcollective/server.cfg diff --git a/doc/mcollective/COPYING b/doc/mcollective/COPYING new file mode 100644 index 0000000..375d1c8 --- /dev/null +++ b/doc/mcollective/COPYING @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2010, 2011 Puppet Labs + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/doc/mcollective/README b/doc/mcollective/README new file mode 100644 index 0000000..8db05de --- /dev/null +++ b/doc/mcollective/README @@ -0,0 +1,6 @@ +The Marionette Collective +========================= + +The Marionette Collective aka. mcollective is a framework to build server orchestration or parallel job execution systems. + +For full information, wikis, ticketing and downloads please see http://marionette-collective.org/ diff --git a/etc/client.cfg.dist b/etc/client.cfg.dist new file mode 100644 index 0000000..514c1a5 --- /dev/null +++ b/etc/client.cfg.dist @@ -0,0 +1,22 @@ +topicprefix = /topic/ +main_collective = mcollective +collectives = mcollective +libdir = /usr/libexec/mcollective +logger_type = console +loglevel = warn + +# Plugins +securityprovider = psk +plugin.psk = unset + +connector = activemq +plugin.activemq.pool.size = 1 +plugin.activemq.pool.1.host = stomp1 +plugin.activemq.pool.1.port = 6163 +plugin.activemq.pool.1.user = mcollective +plugin.activemq.pool.1.password = marionette + +# Facts +factsource = yaml +plugin.yaml = /etc/mcollective/facts.yaml + diff --git a/etc/data-help.erb b/etc/data-help.erb new file mode 100644 index 0000000..eb2d543 --- /dev/null +++ b/etc/data-help.erb @@ -0,0 +1,28 @@ +<%= metastring %> +QUERY FUNCTION INPUT: + +% if entities[:data][:input][:query] + Description: <%= entities[:data][:input][:query][:description] %> + Prompt: <%= entities[:data][:input][:query][:prompt] %> + Type: <%= entities[:data][:input][:query][:type] %> +% if entities[:data][:input][:query][:type] == :string + Validation: <%= entities[:data][:input][:query][:validation] %> + Length: <%= entities[:data][:input][:query][:maxlength] %> +% elsif entities[:data][:input][:query][:type] == :list + Valid Values: <%= entities[:data][:input][:query][:list].join(", ") %> +% end +% if entities[:data][:input][:query][:default] + Default Value: <%= entities[:data][:input][:query][:default] %> +% end +% else + This plugin does not take any input +% end + +QUERY FUNCTION OUTPUT: + +% entities[:data][:output].keys.sort.each do |output| + <%= output %>: + Description: <%= entities[:data][:output][output][:description] %> + Display As: <%= entities[:data][:output][output][:display_as] %> + +% end diff --git a/etc/discovery-help.erb b/etc/discovery-help.erb new file mode 100644 index 0000000..97ceab8 --- /dev/null +++ b/etc/discovery-help.erb @@ -0,0 +1,12 @@ +<%= metastring %> +DISCOVERY METHOD CAPABILITIES: +% [["Filter based on configuration management classes", :classes], +% ["Filter based on system facts", :facts], +% ["Filter based on mcollective identity", :identity], +% ["Filter based on mcollective agents", :agents], +% ["Compound filters combining classes and facts", :compound]].each do |cap| +% if entities[:discovery][:capabilities].include?(cap.last) + <%= cap.first %> +% end +% end + diff --git a/etc/facts.yaml.dist b/etc/facts.yaml.dist new file mode 100644 index 0000000..1afc717 --- /dev/null +++ b/etc/facts.yaml.dist @@ -0,0 +1,2 @@ +--- +mcollective: 1 diff --git a/etc/metadata-help.erb b/etc/metadata-help.erb new file mode 100644 index 0000000..b653db1 --- /dev/null +++ b/etc/metadata-help.erb @@ -0,0 +1,18 @@ +<%= meta[:name] %> +<%= "=" * meta[:name].size %> + +<%= meta[:description] %> + + Author: <%= meta[:author] %> + Version: <%= meta[:version] %> + License: <%= meta[:license] %> + Timeout: <%= meta[:timeout] %> + Home Page: <%= meta[:url] %> +% if requirements[:mcollective] + + Requires MCollective <%= requirements[:mcollective] %> or newer +% end +% unless @usage == "" + +<%= Util.align_text(@usage, nil, 3) if @usage != "" %> +% end diff --git a/etc/msg-help.erb b/etc/msg-help.erb new file mode 100644 index 0000000..c6ade31 --- /dev/null +++ b/etc/msg-help.erb @@ -0,0 +1,11 @@ +Help for message: <%= msg_code %> + +Example Message: + +<%= Util.align_text(msg_example, nil, 3) %> + +Message Detail: + +<%= Util.align_text(msg_detail, nil, 3) %> + +For more information please see http://docs.puppetlabs.com/mcollective/messages/<%= msg_code %>.html diff --git a/etc/rpc-help.erb b/etc/rpc-help.erb new file mode 100644 index 0000000..4ab82a1 --- /dev/null +++ b/etc/rpc-help.erb @@ -0,0 +1,44 @@ +<%= metastring %> +ACTIONS: +======== + <%= entities.keys.sort.join(", ") %> + +% entities.keys.sort.each do |action| + <%= action %> action: + <% (action.size + 8).times do %>-<% end %> + <%= entities[action][:description] %> + + INPUT: +% if entities[action][:input].size > 0 +% entities[action][:input].keys.sort.each do |input| + <%= input %>: + Description: <%= entities[action][:input][input][:description] %> + Prompt: <%= entities[action][:input][input][:prompt] %> + Type: <%= entities[action][:input][input][:type] %> + Optional: <%= !!entities[action][:input][input][:optional] %> +% if entities[action][:input][input][:type] == :string + Validation: <%= entities[action][:input][input][:validation] %> + Length: <%= entities[action][:input][input][:maxlength] %> +% elsif entities[action][:input][input][:type] == :list + Valid Values: <%= entities[action][:input][input][:list].join(", ") %> +% end +% if entities[action][:input][input][:default] + Default Value: <%= entities[action][:input][input][:default] %> +% end + +% end +% else + This action does not have any inputs +% end + + OUTPUT: +% entities[action][:output].keys.sort.each do |output| + <%= output %>: + Description: <%= entities[action][:output][output][:description] %> + Display As: <%= entities[action][:output][output][:display_as] %> +% if entities[action][:output][output][:default] + Default Value: <%= entities[action][:output][output][:default] %> +% end + +% end +% end diff --git a/etc/server.cfg.dist b/etc/server.cfg.dist new file mode 100644 index 0000000..beed021 --- /dev/null +++ b/etc/server.cfg.dist @@ -0,0 +1,23 @@ +topicprefix = /topic/ +main_collective = mcollective +collectives = mcollective +libdir = /usr/libexec/mcollective +logfile = /var/log/mcollective.log +loglevel = info +daemonize = 1 + +# Plugins +securityprovider = psk +plugin.psk = unset + +connector = activemq +plugin.activemq.pool.size = 1 +plugin.activemq.pool.1.host = stomp1 +plugin.activemq.pool.1.port = 6163 +plugin.activemq.pool.1.user = mcollective +plugin.activemq.pool.1.password = marionette + +# Facts +factsource = yaml +plugin.yaml = /etc/mcollective/facts.yaml + diff --git a/etc/ssl/PLACEHOLDER b/etc/ssl/PLACEHOLDER new file mode 100644 index 0000000..e69de29 diff --git a/etc/ssl/clients/PLACEHOLDER b/etc/ssl/clients/PLACEHOLDER new file mode 100644 index 0000000..e69de29 diff --git a/ext/Makefile b/ext/Makefile new file mode 100644 index 0000000..029fda4 --- /dev/null +++ b/ext/Makefile @@ -0,0 +1,44 @@ +#!/usr/bin/make -f + +DESTDIR= + +build: + +clean: + +install: install-bin install-lib install-conf install-plugins install-doc + +install-bin: + install -d $(DESTDIR)/usr/sbin + install -d $(DESTDIR)/usr/bin + cp bin/mc-* $(DESTDIR)/usr/sbin + cp bin/mco $(DESTDIR)/usr/bin + cp bin/mcollectived $(DESTDIR)/usr/sbin/mcollectived + +install-lib: + install -d $(DESTDIR)/usr/lib/ruby/1.8/ + cp -a lib/* $(DESTDIR)/usr/lib/ruby/1.8/ + +install-conf: + install -d $(DESTDIR)/etc/mcollective/ + install -d $(DESTDIR)/etc/init.d + cp -r etc/* $(DESTDIR)/etc/mcollective/ + cp mcollective.init $(DESTDIR)/etc/init.d/mcollective + rm $(DESTDIR)/etc/mcollective/ssl/PLACEHOLDER + rm $(DESTDIR)/etc/mcollective/ssl/clients/PLACEHOLDER + +install-plugins: + install -d $(DESTDIR)/usr/share/mcollective/ + cp -a plugins $(DESTDIR)/usr/share/mcollective/ + +install-doc: + install -d $(DESTDIR)/usr/share/doc/ + cp -a doc $(DESTDIR)/usr/share/doc/mcollective + +uninstall: + rm -f $(DESTDIR)/usr/sbin/mcollectived + rm -rf $(DESTDIR)/usr/lib/ruby/1.8/mcollective* + rm -rf $(DESTDIR)/usr/share/mcollective + rm -rf $(DESTDIR)/etc/mcollective + +.PHONY: build clean install uninstall diff --git a/ext/action_helpers/perl/.gitignore b/ext/action_helpers/perl/.gitignore new file mode 100644 index 0000000..0b5bd39 --- /dev/null +++ b/ext/action_helpers/perl/.gitignore @@ -0,0 +1,3 @@ +Makefile +blib +pm_to_blib diff --git a/ext/action_helpers/perl/Makefile.PL b/ext/action_helpers/perl/Makefile.PL new file mode 100644 index 0000000..6acf3a4 --- /dev/null +++ b/ext/action_helpers/perl/Makefile.PL @@ -0,0 +1,9 @@ +#!perl +use strict; +use ExtUtils::MakeMaker; +WriteMakefile( + NAME => "MCollective::Action", + PREREQ_PM => { + "JSON" => 0, + }, +); diff --git a/ext/action_helpers/perl/lib/MCollective/Action.pm b/ext/action_helpers/perl/lib/MCollective/Action.pm new file mode 100644 index 0000000..b8a63a4 --- /dev/null +++ b/ext/action_helpers/perl/lib/MCollective/Action.pm @@ -0,0 +1,158 @@ +package MCollective::Action; +use strict; +use warnings; +use JSON; + +=head1 NAME + +MCollective::Action - helper class for writing mcollective actions in perl + +=head1 SYNOPSIS + + +In your mcollective agent + + action "echo" do + validate :message, String + + implemented by "/tmp/echo.perl" + end + +And C + + #!/usr/bin/env perl + use strict; + use MCollective::Action; + + my $mc = MCollective::Action->new; + $mc->reply->{message} = $mc->request->{message}; + $mc->reply->{timestamp} = time; + $mc->info("some text to log on the server"); + + +=head1 DESCRIPTION + +mcollective version 1.X introduced a mechanism for writing agent actions as +external commands. This module provides a convenient api for writing them in +perl which performs some of the boilerplate for you. + +=head2 METHODS + +=over + +=item new + +create a new MCollection::Action helper object + +=cut + +sub new { + my $class = shift; + my $self = bless { + request => {}, + reply => {}, + }, $class; + $self->_load; + return $self; +} + +=item request + +returns a hash reference containing the request + +=cut + + +sub request { $_[0]->{request} } + + +=item reply + +returns a hash reference you should populate with your reply + +=cut + +sub reply { $_[0]->{reply} } + + +sub _load { + my $self = shift; + my $file = $ENV{MCOLLECTIVE_REQUEST_FILE}; + open my $fh, "<$file" + or die "Can't open '$file': $!"; + my $json = do { local $/; <$fh> }; + $self->{request} = JSON->new->decode( $json ); + delete $self->request->{data}{process_results}; +} + +sub DESTROY { + my $self = shift; + $self->_save; +} + +sub _save { + my $self = shift; + my $file = $ENV{MCOLLECTIVE_REPLY_FILE}; + open my $fh, ">$file" + or die "Can't open '$file': $!"; + print $fh JSON->new->encode( $self->reply ); +} + +=item info($message) + +report a message into the server log + +=cut + +sub info { + my ($self, $message) = @_; + print STDOUT $message, "\n"; +} + +=item error($message) + +report an error into the server log + +=cut + + +sub error { + my ($self, $message) = @_; + print STDERR $message, "\n"; +} + +=item fail + +reports an error and exits immediately + +=cut + +sub fail { + my ($self, $message) = @_; + $self->error( $message ); + exit 1; +} + +1; + +__END__ + +=back + +=head1 AUTHOR + +Richard Clamp + +=head1 COPYRIGHT + +Copyright 2011, Richard Clamp. All Rights Reserved. + +This program is free software; you can redistribute it +and/or modify it under the same terms as Perl itself. + +=head1 SEE ALSO + +http://docs.puppetlabs.com/mcollective/ + +=cut + diff --git a/ext/action_helpers/perl/t/basic.t b/ext/action_helpers/perl/t/basic.t new file mode 100644 index 0000000..4717efb --- /dev/null +++ b/ext/action_helpers/perl/t/basic.t @@ -0,0 +1,30 @@ +#!perl +use strict; +use Test::More; +use JSON; +use File::Temp; + +my $class = "MCollective::Action"; +use_ok( $class ); + +my $infile = File::Temp->new; +my $outfile = File::Temp->new; + +$ENV{MCOLLECTIVE_REQUEST_FILE} = $infile->filename; +$ENV{MCOLLECTIVE_REPLY_FILE} = $outfile->filename; +print $infile JSON->new->encode({ red => "apples", blue => "moon" }); +close $infile; +{ + my $mc = $class->new; + isa_ok( $mc, $class ); + is( $mc->request->{red}, "apples", "apples are red" ); + $mc->reply->{potato} = "chips"; +} + +my $json = do { local $/; <$outfile> }; +ok( $json, "Got some JSON" ); +my $reply = JSON->new->decode( $json ); + +is( $reply->{potato}, "chips", "Got the reply that potato = chips" ); + +done_testing(); diff --git a/ext/action_helpers/php/README.markdown b/ext/action_helpers/php/README.markdown new file mode 100644 index 0000000..ad677f6 --- /dev/null +++ b/ext/action_helpers/php/README.markdown @@ -0,0 +1,38 @@ +A simple helper to assist with writing MCollective actions in PHP. + +Given an action as below: + +
+action "echo" do
+   validate :message, String
+
+   implemented_by "/tmp/echo.php"
+end
+
+ +The following PHP script will implement the echo action externally +replying with _message_ and _timestamp_ + +
+<?php
+    require("mcollective_action.php");
+
+    $mc = new MCollectiveAction();
+    $mc->message = $mc->data["message"];
+    $mc->timestamp = strftime("%c");
+    $mc->info("some text to info log on the server");
+?>
+
+ +Calling it with _mco rpc_ results in: + +
+$ mco rpc test echo message="hello world"
+Determining the amount of hosts matching filter for 2 seconds .... 1
+
+ * [ ============================================================> ] 1 / 1
+
+
+nephilim.ml.org                         : OK
+    {:message=>"hello world", :time=>"Tue Mar 15 19:20:53 +0000 2011"}
+
diff --git a/ext/action_helpers/php/mcollective_action.php b/ext/action_helpers/php/mcollective_action.php new file mode 100644 index 0000000..6487668 --- /dev/null +++ b/ext/action_helpers/php/mcollective_action.php @@ -0,0 +1,65 @@ +infile = $_ENV["MCOLLECTIVE_REQUEST_FILE"]; + $this->outfile = $_ENV["MCOLLECTIVE_REPLY_FILE"]; + + $this->readJSON(); + } + + function __destruct() { + $this->save(); + } + + function readJSON() { + $this->request = json_decode(file_get_contents($this->infile), true); + unset($this->request["data"]["process_results"]); + } + + function save() { + file_put_contents($this->outfile, json_encode($this->request["data"])); + } + + // prints a line to STDERR that will log at error level in the + // mcollectived log file + function error($msg) { + fwrite(STDERR, "$msg\n"); + } + + // prints a line to STDOUT that will log at info level in the + // mcollectived log file + function info($msg) { + fwrite(STDOUT, "$msg\n"); + } + + // logs an error message and exits with RPCAborted + function fail($msg) { + $this->error($msg); + exit(1); + } + + function __get($property) { + if (isSet($this->request[$property])) { + return $this->request[$property]; + } else { + throw new Exception("No $property in request"); + } + } + + function __set($property, $value) { + $this->request["data"][$property] = $value; + } +} +?> diff --git a/ext/action_helpers/python/kwilczynski/README.markdown b/ext/action_helpers/python/kwilczynski/README.markdown new file mode 100644 index 0000000..d73f3fb --- /dev/null +++ b/ext/action_helpers/python/kwilczynski/README.markdown @@ -0,0 +1,46 @@ +A simple helper to assist with writing MCollective actions in Python. + +Given an action as below: + +
+action "echo" do
+   validate :message, String
+
+   implemented_by "/tmp/echo.py"
+end
+
+ +The following Python script will implement the echo action externally +replying with _message_ and current _time_. + +
+#!/usr/bin/env python
+
+import sys
+import time
+import mcollective_action as mc
+
+if __name__ == '__main__':
+    mc = mc.MCollectiveAction()
+    request = mc.request()
+    mc.message = request['data']['message']
+    mc.time = time.strftime('%c')
+    mc.info("Some text to info log in the server")
+
+    sys.exit(0)
+
+ +Calling it with _mco rpc_ results in: + +
+$ mco rpc test echo message="Hello World"
+Determining the amount of hosts matching filter for 2 seconds .... 1
+
+ * [ ============================================================> ] 1 / 1
+
+
+host.example.com              : OK
+    {:message=>"Hello World", :time=>"Tue Mar 15 19:20:53 +0000 2011"}
+
+ +This implementation was successfully tested with Python 2.4 and 2.6. diff --git a/ext/action_helpers/python/kwilczynski/echo.py b/ext/action_helpers/python/kwilczynski/echo.py new file mode 100644 index 0000000..bc22b63 --- /dev/null +++ b/ext/action_helpers/python/kwilczynski/echo.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +import sys +import time +import mcollective_action as mc + +if __name__ == '__main__': + mc = mc.MCollectiveAction() + request = mc.request() + mc.message = request['data']['message'] + mc.time = time.strftime('%c') + mc.info("An example echo agent") + + sys.exit(0) + +# vim: set ts=4 sw=4 et : diff --git a/ext/action_helpers/python/kwilczynski/mcollective_action.py b/ext/action_helpers/python/kwilczynski/mcollective_action.py new file mode 100644 index 0000000..84c182a --- /dev/null +++ b/ext/action_helpers/python/kwilczynski/mcollective_action.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python + +import os +import sys + +class Error(Exception): + pass + +class MissingModule(Error): + pass + +class MissingFiles(Error): + pass + +class MissingEnvironemntVariable(Error): + pass + +class FileReadError(Error): + pass + +class JSONParsingError(Error): + pass + +try: + import simplejson as json +except ImportError: + raise MissingModule('Unable to load JSON module. Missing module?') + +class MCollectiveAction(object): + + _environment_variables = [ 'MCOLLECTIVE_REQUEST_FILE', + 'MCOLLECTIVE_REPLY_FILE' ] + + def __init__(self): + self._info = sys.__stdout__ + self._error = sys.__stderr__ + + for entry in '_reply', '_request': + self.__dict__[entry] = {} + + self._arguments = sys.argv[1:] + + if len(self._arguments) < 2: + try: + for variable in self._environment_variables: + self._arguments.append(os.environ[variable]) + except KeyError: + raise MissingEnvironemntVariable("Environment variable `%s' " + "is not set." % variable) + + self._request_file, self._reply_file = self._arguments + + if len(self._request_file) == 0 or len(self._reply_file) == 0: + raise MissingFiles("Both request and reply files have to be set.") + + def __setattr__(self, name, value): + if name.startswith('_'): + object.__setattr__(self, name, value) + else: + self.__dict__['_reply'][name] = value + + def __getattr__(self, name): + if name.startswith('_'): + return self.__dict__.get(name, None) + else: + return self.__dict__['_reply'].get(name, None) + + def __del__(self): + if self._reply: + try: + file = open(self._reply_file, 'w') + json.dump(self._reply, file) + file.close() + except IOError, error: + raise FileReadError("Unable to open reply file `%s': %s" % + (self._reply_file, error)) + + def info(self, message): + print >> self._info, message + self._info.flush() + + def error(self, message): + print >> self._error, message + self._error.flush() + + def fail(self, message, exit_code=1): + self.error(message) + sys.exit(exit_code) + + def reply(self): + return self._reply + + def request(self): + if self._request: + return self._request + else: + try: + file = open(self._request_file, 'r') + self._request = json.load(file) + file.close() + except IOError, error: + raise FileReadError("Unable to open request file `%s': %s" % + (self._request_file, error)) + except json.JSONDecodeError, error: + raise JSONParsingError("An error occurred during parsing of " + "the JSON data in the file `%s': %s" % + (self._request_file, error)) + file.close() + + return self._request + +# vim: set ts=4 sw=4 et : diff --git a/ext/action_helpers/python/romke/README.markdown b/ext/action_helpers/python/romke/README.markdown new file mode 100644 index 0000000..fe1c62e --- /dev/null +++ b/ext/action_helpers/python/romke/README.markdown @@ -0,0 +1,38 @@ +A simple helper to assist with writing MCollective actions in Python. + +Given an action as below: + +
+action "echo" do
+   validate :message, String
+
+   implemented_by "/tmp/echo.py"
+end
+
+ +The following Python script will implement the echo action externally +replying with _message_ and _timestamp_ + +
+#!/bin/env python
+import mcollectiveah
+import time
+
+mc = mcollectiveah.MCollectiveAction()
+mc.reply['message'] = mc.request['message']
+mc.reply['timestamp'] = time.strftime("%c")
+mc.reply['info'] = "some text to info log in the server"
+
+ +Calling it with _mco rpc_ results in: + +
+$ mco rpc test echo message="hello world"
+Determining the amount of hosts matching filter for 2 seconds .... 1
+
+ * [ ============================================================> ] 1 / 1
+
+
+nephilim.ml.org                         : OK
+    {:message=>"hello world", :time=>"Tue Mar 15 19:20:53 +0000 2011"}
+
diff --git a/ext/action_helpers/python/romke/mcollectiveah.py b/ext/action_helpers/python/romke/mcollectiveah.py new file mode 100644 index 0000000..bca8ab6 --- /dev/null +++ b/ext/action_helpers/python/romke/mcollectiveah.py @@ -0,0 +1,69 @@ +#!/bin/env python +# -*- coding: utf-8 -*- vim: set ts=4 et sw=4 fdm=indent : +import os, sys + +try: + import simplejson +except ImportError: + sys.stderr.write('Unable to load simplejson python module.') + sys.exit(1) + +class MCollectiveActionNoEnv(Exception): + pass +class MCollectiveActionFileError(Exception): + pass + +class MCollectiveAction(object): + def __init__(self, *args, **kwargs): + try: + self.infile = os.environ['MCOLLECTIVE_REQUEST_FILE'] + except KeyError: + raise MCollectiveActionNoEnv("No MCOLLECTIVE_REQUEST_FILE environment variable") + try: + self.outfile = os.environ['MCOLLECTIVE_REPLY_FILE'] + except KeyError: + raise MCollectiveActionNoEnv("No MCOLLECTIVE_REPLY_FILE environment variable") + + self.request = {} + self.reply = {} + + self.load() + + def load(self): + if not self.infile: + return False + try: + infile = open(self.infile, 'r') + self.request = simplejson.load(infile) + infile.close() + except IOError, e: + raise MCollectiveActionFileError("Could not read request file `%s`: %s" % (self.infile, e)) + except simplejson.JSONDecodeError, e: + infile.close() + raise MCollectiveActionFileError("Could not parse JSON data in file `%s`: %s", (self.infile, e)) + + def send(self): + if not getattr(self, 'outfile', None): # if exception was raised during or before setting self.outfile + return False + try: + outfile = open(self.outfile, 'w') + simplejson.dump(self.reply, outfile) + outfile.close() + except IOError, e: + raise MCollectiveActionFileError("Could not write reply file `%s`: %s" % (self.outfile, e)) + + def error(self, msg): + """Prints line to STDERR that will be logged at error level in the mcollectived log file""" + sys.stderr.write("%s\n" % msg) + + def fail(self, msg): + """Logs error message and exitst with RPCAborted""" + self.error(msg) + sys.exit(1) + + def info(self, msg): + """Prints line to STDOUT that will be logged at info level in the mcollectived log file""" + sys.stdout.write("%s\n" % msg) + + def __del__(self): + self.send() diff --git a/ext/action_helpers/python/romke/test.py b/ext/action_helpers/python/romke/test.py new file mode 100644 index 0000000..3bfe083 --- /dev/null +++ b/ext/action_helpers/python/romke/test.py @@ -0,0 +1,50 @@ +#!/bin/env python +# -*- coding: utf-8 -*- vim: set ts=4 et sw=4 fdm=indent : +import unittest, tempfile, simplejson, os, random + +import mcollectiveah + +class TestFunctions(unittest.TestCase): + def test_raise_environ(self): + try: + del os.environ['MCOLLECTIVE_REQUEST_FILE'] + del os.environ['MCOLLECTIVE_REPLY_FILE'] + except: pass + self.assertRaises(mcollectiveah.MCollectiveActionNoEnv, mcollectiveah.MCollectiveAction) + + def test_raise_file_error(self): + os.environ['MCOLLECTIVE_REQUEST_FILE'] = '/tmp/mcollectiveah-test-request.%d' % random.randrange(100000) + os.environ['MCOLLECTIVE_REPLY_FILE'] = '/tmp/mcollectiveah-test-reply.%d' % random.randrange(100000) + + self.assertRaises(mcollectiveah.MCollectiveActionFileError, mcollectiveah.MCollectiveAction) + + os.unlink(os.environ['MCOLLECTIVE_REPLY_FILE']) + + def test_echo(self): + tin = tempfile.NamedTemporaryFile(mode='w', delete=False) + self.data = {'message': 'test'} + + simplejson.dump(self.data, tin) + os.environ['MCOLLECTIVE_REQUEST_FILE'] = tin.name + tin.close() + + tout = tempfile.NamedTemporaryFile(mode='w') + os.environ['MCOLLECTIVE_REPLY_FILE'] = tout.name + tout.close() + + mc = mcollectiveah.MCollectiveAction() + mc.reply['message'] = mc.request['message'] + del mc + + tout = open(os.environ['MCOLLECTIVE_REPLY_FILE'], 'r') + data = simplejson.load(tout) + tout.close() + + self.assertEqual(data, self.data) + + + os.unlink(os.environ['MCOLLECTIVE_REQUEST_FILE']) + os.unlink(os.environ['MCOLLECTIVE_REPLY_FILE']) + +if __name__ == '__main__': + unittest.main() diff --git a/ext/activemq/apache-activemq.spec b/ext/activemq/apache-activemq.spec new file mode 100644 index 0000000..e98bdd4 --- /dev/null +++ b/ext/activemq/apache-activemq.spec @@ -0,0 +1,206 @@ +Summary: Apache ActiveMQ +Name: activemq +Version: 5.3.0 +Release: 1%{?dist} +License: Apache +Group: Network/Daemons +Source0: apache-activemq-%{version}-bin.tar.gz +Source1: wlcg-patch.tgz +BuildRoot: %{_tmppath}/%{name}-%{version}-root +BuildArch: noarch +Requires: tanukiwrapper >= 3.2.0 + +#%define buildver 5.1.0 + +%define homedir /usr/share/%{name} +%define libdir /var/lib/%{name} +%define libexecdir /usr/libexec/%{name} +%define cachedir /var/cache/%{name} +%define docsdir /usr/share/doc/%{name}-%{version} + +%description +ApacheMQ is a JMS Compliant Messaging System + +%package info-provider +Summary: An LDAP information provider for activemq +Group:grid/lcg +%description info-provider +An LDAP infomation provider for activemq + +%package meta +Summary: A metapackage +Group:grid/lcg +Requires: activemq = ${version}-${release}, activemq-info-provider = ${version}-${release} +%description meta +A metapackage + +%prep +%setup -q -a1 -n apache-activemq-%{version} + +%build +install --directory ${RPM_BUILD_ROOT} + +%install +rm -rf $RPM_BUILD_ROOT +install --directory ${RPM_BUILD_ROOT}%{homedir} +install --directory ${RPM_BUILD_ROOT}%{homedir}/bin +install --directory ${RPM_BUILD_ROOT}%{docsdir} +install --directory ${RPM_BUILD_ROOT}%{libdir}/lib +install --directory ${RPM_BUILD_ROOT}%{libexecdir} +install --directory ${RPM_BUILD_ROOT}%{libdir}/webapps +install --directory ${RPM_BUILD_ROOT}%{cachedir} +install --directory ${RPM_BUILD_ROOT}%{cachedir}/data +install --directory ${RPM_BUILD_ROOT}/var/log/%{name} +install --directory ${RPM_BUILD_ROOT}/var/run/%{name} +install --directory ${RPM_BUILD_ROOT}/etc/%{name} +install --directory ${RPM_BUILD_ROOT}/etc/init.d +install --directory ${RPM_BUILD_ROOT}/etc/httpd/conf.d + +# Config files +install conf/activemq.xml ${RPM_BUILD_ROOT}/etc/%{name} +install conf/credentials.properties ${RPM_BUILD_ROOT}/etc/%{name} +install conf/jetty.xml ${RPM_BUILD_ROOT}/etc/%{name} +install conf/log4j.properties ${RPM_BUILD_ROOT}/etc/%{name} +install conf/activemq-wrapper.conf ${RPM_BUILD_ROOT}/etc/%{name} +install conf/activemq-httpd.conf ${RPM_BUILD_ROOT}/etc/httpd/conf.d + +# startup script +install bin/activemq ${RPM_BUILD_ROOT}/etc/init.d + +# Bin and doc dirs +install *.txt *.html ${RPM_BUILD_ROOT}%{docsdir} +cp -r docs ${RPM_BUILD_ROOT}%{docsdir} + +install bin/run.jar bin/activemq-admin ${RPM_BUILD_ROOT}%{homedir}/bin +install --directory ${RPM_BUILD_ROOT}/usr/bin +%{__ln_s} -f %{homedir}/bin/activemq-admin ${RPM_BUILD_ROOT}/usr/bin + +# Runtime directory +cp -r lib ${RPM_BUILD_ROOT}%{libdir} +cp -r webapps/admin ${RPM_BUILD_ROOT}%{libdir}/webapps + +# Info provider +install info-provider-activemq ${RPM_BUILD_ROOT}/%{libexecdir} + +pushd ${RPM_BUILD_ROOT}%{homedir} + [ -d conf ] || %{__ln_s} -f /etc/%{name} conf + [ -d data ] || %{__ln_s} -f %{cachedir}/data data + [ -d docs ] || %{__ln_s} -f %{docsdir} docs + [ -d lib ] || %{__ln_s} -f %{libdir}/lib lib + [ -d lib ] || %{__ln_s} -f %{libdir}/libexec libexec + [ -d log ] || %{__ln_s} -f /var/log/%{name} log + [ -d webapps ] || %{__ln_s} -f %{libdir}/webapps webapps +popd + + +%pre +# Add the "activemq" user and group +# we need a shell to be able to use su - later +/usr/sbin/groupadd -g 92 -r activemq 2> /dev/null || : +/usr/sbin/useradd -c "Apache Activemq" -u 92 -g activemq \ + -s /bin/bash -r -d /usr/share/activemq activemq 2> /dev/null || : + +%post +# install activemq (but don't activate) +/sbin/chkconfig --add activemq + +%preun +if [ $1 = 0 ]; then + [ -f /var/lock/subsys/activemq ] && /etc/init.d/activemq stop + [ -f /etc/init.d/activemq ] && /sbin/chkconfig --del activemq +fi + +%postun + +%clean +rm -rf $RPM_BUILD_ROOT + + +%files +%defattr(-,root,root) +%attr(755,root,root) /usr/bin/activemq-admin +%{homedir} +%docdir %{docsdir} +%{docsdir} +%{libdir} +%attr(775,activemq,activemq) %dir /var/log/%{name} +%attr(775,activemq,activemq) %dir /var/run/%{name} +%attr(775,root,activemq) %dir %{cachedir}/data +%attr(755,root,root) /etc/init.d/activemq +%config(noreplace) /etc/httpd/conf.d/activemq-httpd.conf +%config(noreplace) /etc/%{name}/activemq.xml +%config(noreplace) %attr(750,root,activemq) /etc/%{name}/credentials.properties +%config(noreplace) /etc/%{name}/jetty.xml +%config(noreplace) /etc/%{name}/activemq-wrapper.conf +%config(noreplace) /etc/%{name}/log4j.properties + +%files info-provider +%defattr(-,root,root) +%attr(755,root,root) %{libexecdir}/info-provider-activemq + +%changelog +* Sat Jan 16 2010 R.I.Pienaar 5.3.0 +- Adjusted for ActiveMQ 5.3.0 + +* Wed Oct 29 2008 James Casey 5.2.0-2 +- fixed defattr on subpackages + +* Tue Sep 02 2008 James Casey 5.2.0-1 +- Upgraded to activemq 5.2.0 + +* Tue Sep 02 2008 James Casey 5.1.0-7 +- Added separate logging of messages whenever the logging interceptor is enabled in the config file +- removed BrokerRegistry messages casued by REST API +- now we don't log messages to stdout (so no duplicates in wrapper log). +- upped the number and size of the rolling logs + +* Fri Aug 29 2008 James Casey 5.1.0-6 +- make ServiceData be correct LDIF + +* Wed Aug 27 2008 James Casey 5.1.0-5 +- changed glue path from mds-vo-name=local to =resource + +* Tue Aug 05 2008 James Casey 5.1.0-4 +- fixed up info-provider to give both REST and STOMP endpoints + +* Mon Aug 04 2008 James Casey 5.1.0-3 +- reverted out APP_NAME change to ActiveMQ from init.d since it + causes too many problems +* Mon Aug 04 2008 James Casey 5.1.0-2 +- Added info-provider +- removed mysql as a requirement + +* Thu Mar 20 2008 Daniel RODRIGUES - 5.1-SNAPSHOT-1 +- Changed to version 5.1 SNAPSHOT of 18 Mar, fizing AMQ Message Store +- small fixes to makefile + +* Fri Dec 14 2007 James CASEY - 5.0.0-3rc4 +- Added apache config file to forward requests to Jetty + +* Thu Dec 13 2007 James CASEY - 5.0.0-2rc4 +- fixed /usr/bin symlink +- added useJmx to the default config + +* Thu Dec 13 2007 James CASEY - 5.0.0-RC4.1 +- Moved to RC4 of the 5.0.0 release candidates + +* Mon Dec 10 2007 James CASEY - 5.0-SNAPSHOT-7 +- added symlink in /usr/bin for activemq-admin + +* Wed Nov 26 2007 James CASEY - 5.0-SNAPSHOT-6 +- fix bug with group name setting in init.d script + +* Wed Nov 26 2007 James CASEY - 5.0-SNAPSHOT-5 +- fix typos in config file for activemq + +* Wed Nov 26 2007 James CASEY - 5.0-SNAPSHOT-4 +- add support for lib64 version of tanukiwrapper in config +- turned off mysql persistence in the "default" config + +* Wed Oct 17 2007 James CASEY - 5.0-SNAPSHOT-2 +- more re-org to mirror how tomcat is installed. +- support for running as activemq user + +* Tue Oct 16 2007 James CASEY - 5.0-SNAPSHOT-1 +- Initial Version + diff --git a/ext/activemq/examples/multi-broker/README b/ext/activemq/examples/multi-broker/README new file mode 100644 index 0000000..4922916 --- /dev/null +++ b/ext/activemq/examples/multi-broker/README @@ -0,0 +1,12 @@ +3 ActiveMQ servers clustered together in a star foramt: + + + broker2 <-----> broker1 <----> broker3 + + +Pay attention to the names in the config file as well +as the users. This is identical to the simple single +broker example except with the aded amq user for the +clustering and the connection setups in broker1 + +Tested to work with ActiveMQ 5.5.0 diff --git a/ext/activemq/examples/multi-broker/broker1-activemq.xml b/ext/activemq/examples/multi-broker/broker1-activemq.xml new file mode 100755 index 0000000..a910b3d --- /dev/null +++ b/ext/activemq/examples/multi-broker/broker1-activemq.xml @@ -0,0 +1,133 @@ + + + + + file:${activemq.base}/conf/credentials.properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/activemq/examples/multi-broker/broker2-activemq.xml b/ext/activemq/examples/multi-broker/broker2-activemq.xml new file mode 100755 index 0000000..163cf36 --- /dev/null +++ b/ext/activemq/examples/multi-broker/broker2-activemq.xml @@ -0,0 +1,73 @@ + + + + + file:${activemq.base}/conf/credentials.properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/activemq/examples/multi-broker/broker3-activemq.xml b/ext/activemq/examples/multi-broker/broker3-activemq.xml new file mode 100755 index 0000000..d118852 --- /dev/null +++ b/ext/activemq/examples/multi-broker/broker3-activemq.xml @@ -0,0 +1,73 @@ + + + + + file:${activemq.base}/conf/credentials.properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/activemq/examples/single-broker/README b/ext/activemq/examples/single-broker/README new file mode 100644 index 0000000..0986867 --- /dev/null +++ b/ext/activemq/examples/single-broker/README @@ -0,0 +1,5 @@ +Simple single broker setup for ActiveMQ 5.5.0. + +Provides 2 users, one admin and one for mcollective. +Admin user can create all sorts of queues and topics, +mcollective user is restricted. diff --git a/ext/activemq/examples/single-broker/activemq.xml b/ext/activemq/examples/single-broker/activemq.xml new file mode 100644 index 0000000..bb9ea1b --- /dev/null +++ b/ext/activemq/examples/single-broker/activemq.xml @@ -0,0 +1,72 @@ + + + + + file:${activemq.base}/conf/credentials.properties + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/activemq/wlcg-patch.tgz b/ext/activemq/wlcg-patch.tgz new file mode 100644 index 0000000000000000000000000000000000000000..469169726c3fa4116c04ae5bb498580bec0620f2 GIT binary patch literal 9890 zcmV;TCSBPdiwFSF(ostQ1MEC&bK1zV`>Z%#t{iZA~7 zT77Jme{*klw*m66?e6W@o3%zAs4913>6ZT$z)IIF5sqJuP*+Mpg&X;yL2Rz~27HsJSmuw1{ zOv|#dW7A=_3lA2OjDXL~z_Q&LbnD*)_G}(9Z|U+t%x&KQEF&Do@K{qKBwFed2KlQO zsWGI6tGKYkegQPtNyDfCre_FJRxiuXU$7f!yfAN=>xC>5JcXHGPq-ffp+MEbcWl$0 z@MKtV55S9G6%KD4ngDLnFqk(@gE2FNcJMb3L;s*!T`re~Nu(NHFstg3RZoDMy|J{>X!l0l&XDy7?6BWE>W;eo9{f6H z?cT5KO}BTn!#HpXy5ZM;fb;{AHgb_$C{V3EngCP|{4IPwv8VO~#^uf;bH>@syW)Y1 zy5aL+VGER{0P-xr;@As2Boz{?gEAh|ssLA8G97y>wa?DZd+pOstE@eEnVy~Wd#}`s zHoZ9gH+va)mpp*?hokN(ZX%?7r+JR!UlHPj>AkxfMw2H@U8IOVcp(HsW2OL2B%EGJAeV)-B-Q-pmRQi z|Lv1uD-0s8XpcFvlOdV#1JNiAQ>2XuEnR?Iu=(j(e=q`@WfgX6UIIuIpaFt7u>pdy zxoRCd8W6`740wh-^2n*6BOw^zwe*oNh*rHuFA63YSP_%& z1tD|6Q47r&$3cDO%0hI+CMw6A0^%GTAzH!UZSTB2Jbydv3|iVyODZ%@J;1X>@NnsY zS9C#S80l$z5HAqtWPXX}Y|pfI8tjm8<7jOfC=5REfDj;vY!^afpkoaV5#$QJ-GF^~ zk%NOMGavXhBM|j9&nvvvhUe0gwV%w>9JpbrBnk13f}0C2pksKj&Fu@Ef7h3X<~wo zZq-2UB?h1B56|g8eujsG?%4=DYponVd+~z4rl8K=9knj}rFF5|4qjDrjkz!9>JC=+8!NxxQqRlm`xg8xv)@aW zM?F5#${-*mkp^ErC_@4vJB0`a3O)gA1EuxNK%lf!>`k8Df+VxJi5T?PH&3-;pR z#ccaRb)tEl41gGn{+>9zLl)y)Nw-Tw8hZ|=12`^1GU~s9{AfduRp?XqIOw!b&f25b zE&7EzeTYW8C6T;fYV;Y<@|ONu-8xL)18%!~;Rsd%8%cp)l(SIX z-W!0!^a!9%AkR8r+r|%-pV@z&89)5z`PO!>0fa~m3{45VYW%Rxer8pRUsYXD-xtVS zH`6LNG-{dw;Ev!MT3lFmfL59vNx52pX==wmEhr{RnJ=+|p<01c3j`aRlS`DpR5>Fn zFeP98J0sx(CE=1{%WoNpfMzSTK>bFL3{$jelqK<*;m5`pV=JkF(Kt%+AjqLcK>&Ua z66b(VpiH{a&cey1DB5{|HWYKSgx@iDk*-08dlhEkF$SH0J0|-49g1XjC@`@iPavP` zzR`)rOvH!gl}GuP2O)R?j((~m8rEcJW(oPw4IqQa6UUt6?qE1N2aXN2JQPKrVDurY zOzJ!Ji;|K+0xaE2711^0}-$_+8 zK6FC-`JPp-L9y_4Mqw1ZhK+4K3%_FDB$M)6SLd8E!6I6Wat`Xu;TX{aPKg=CA(ccz zW(N65lG*2nrbdO8GG$4?vyd;+9boEO*X~2gFU>1Yl#aTf6@3V05xInl0Obh-_S->( zN$A1v@C~v$`bIA*7Cae4VSIV;`-RL!%3BIaS?46%B62;kounMPmooHi2DVs9PqycN zCA!{*KIFIk-J;fWKe}FRYMB9~okj2qbK;4D7LjXTpNp%aCin_xD{jq!%wRHy0$>tC zg(11v9vr@o`RKF8&v8h+7f+SF#waP6MDC>v#UO_IPoE0jXN-B3n#H{+07Xd)R(7IA zYc4Dh4hqUPn+^3vKWYB7FTd>SFMImSzWQ?g)BgFYbcxzByjvBP?Kgfb24C2IfhPGl zFzSlF`IF@Px&Gl#){y|O3Q(HoptP#3S)uR6tx>rP5P&`GM${bgj9_y0kK*R^a<%n8 zYPDMRr&{gKeQJF@f)d=~=pRy3Bn+IHDHDZAPEd7m-lN&5jIq$o0)Yn+A(iBAJdZ#V zserTD!1=WOU4RX#u!O*k)2U(yeI)j{ze_j^aN*l{vJYL_!V`qVu`juUHMScHMIs|F zh>DCweIFZ*k3O}z*qGnZUw~a)@tt9miVDJ9!O8s#w!G%QMhwwt;AI9uF{r4He+Hu@>ENjOT zQ}ca>o?=oql0#h-Cp}m~tAr()BGNW@uu5qb9b*Y3utfy--4d>wP9Kh0c53!wwd0v5?|tgsYM?SLI65S!(%1^=ZVNW*010 zyOx#e*`R+$lW-DT{?wGDK=fLI>=8R`Z6&pC4uJ-i3oGYp5v4U6yzQ(E0gKzr03e;g z)u&0oNv~1>Dubd2$?S+qp(vrl*=Utst9x*TxKbiB% z9Dr;BrYLo>*rs8LrUR&g)nWN1dR8(8~eq>ju}uXvJMA&8o7s<*cG9 z1XLt6y}0nfIL2}t#l&+REp^u>W+Hz=#F5Qw=!Js^`U2Y}S zkIR>8ArNaSEPr(EgVc|C3b|GZx8-_-)r%%`mc)bndvG^PH#5kC483cbW?e-} zGln6}B&ecX&h?Wy2wZ&weGpQ!$)lOB$ZZoKC8doU8ADM%Kxvkl_y*Ma#0`iDXT&lR zt4va>A#AH&P7~Ynma>1GObFD}X<*N6+(Jh5m^0IMX|jQKaj9KU6L8Nim|)Igzev4j zJoIg=P~&kny-;n|sJ!+WYCD1Ux59%#B)c=x7EG3ouD~*iwYV}(6yZL)8!R^H-p0}- zN&7KS)wC1;Tq;RU#feKypIVP7^5YUF$iozpj;jwB5L9E}i}gaek0a`ajP@A_VUzbo zz1udoQMGB^B@XGh=}6^+=(h>b4z)A^q5wH~*Z^}$jZNDaE~?K_8(c7g~a(kkL8Qh*Hu8n{$w8`4fs zFmfoeXzEU^Zqm~;mO7q0FHReXlhXB7{e#the0W{{}m*A8!u3JUE50>Tq1Tlh02&8dBAMetyRDf=h{ zk!k0EFT5*YM;7Tp+_ZR3LM_+jAFsFWl|o+jerUSnt{_Q_d(0)SxuiA8?Z7P{Gt+*7 z>^LVfEvnS_%9Z?|3+?sQ(7x6k=aM76kP7md;kIY+y8$sax25nF@ub))2Z;LWIXoVq zl9{rKt;`Nl!%&<{g_XO}f7m}gZTF7OPr5x^o>gUhR=cx{(z^Nt&)|4715C;dTk&#k z{2IEIwW;>OTS;Zkl2DoQ`+UN-(&Vg8S4kFRhfa2pEj8AzoPI8`5_z&5Q-}EM@5xpD z!ZJbUT>F1G%~)iaoy>ClwT${Ez3n^WinbgQA*sXKh43vf|D0Bx&!WGK( zI2RPB;g<=NZ>4>aZw2xs=y z9p)*B=A0DnJB_g7>6Nd`PP%2so_+s!=RmePddDdvL)G&k@30|Zn^OV=+M-usL(I|j z2$u17<#aVA~`Rj_K?TEDc@slol>M2 zsHl>^Bw-ma;c!S!>e+`X)k(1y_Ydk{BsnF*(z+&_%a|1=N?zc;dzmfYZW=kWSd;US zy9bnOQDss*EF2;cM~ugb1$I#=$c^Amu}{}0&yb4ec|_?7)`}K>F76${qrV{Rt&WbO zQl%*aQ&wFHqSCF3f$;sNgj#Klg1)US!mPAR9bixa z7{kB;o`m?QrGOi>k!bqwVX>xoAYTAM@P$oGUo&G^=gL{dNCGt~H-R%bBi59m$>l!nE}k&<4ZHu#X{=`(cNyXp%bLT)a0=*kp2 z2uZ z?fW2VW9>=$Ag@k72&D4lHpa+a*TYRfAdUh8az-hIO_CRR-dwFh;&g^E@Js+WhhhJu z!${-NJe9X4N2bmWXgyRQh8%b+MS%o3l+{LC!4)?%9P_5I^_dxhak(e-dShGc=t>3; z)u{Efvgfm>m1fOo)+Qh8pTUx!E}puYXL&r~99-VqbELtc2#@K)gf)~G(mU{IBNaAW zH)8BcE-!pclpKod&zM@qH+mqxqLvHu8j%li5u#pe?AG4ATm$yfoLokJ7am==_G(2y z@`pTfUhw)sGTAH;i~aVrQr|P`d*SbFop^xpr)u6(X>7g|?F%2of8BV45Mq!5A;jHfFoKGcA9s1xhEPyl|b|Ne~6w>#gy-TC(I&bR;5cfS4b2%qGpHr#fkGUZ1?U5dAo zU=h$dZl4T0CBZ{OpUBPLIahGyTro6AzZhEgr!EgMEIVkayCafh6H~5#nOA1!#ys8f z1H3TQe!&+0h@nsEGAYMQZH>eqYyokC)QHEmlVa!zs4sVM8!gt%Q*%Q1lYv%&76m3e zOI>lMnw*+0C|ICS;}$&^?8bB;?ZSfx?R1W?y#L4|W=TWYmT-B+aU$4WWH zE_4V*896t>Cp2m#M|uRyn?y)|vi-s0xOYt$S=Qi{TGTfsWbzU}ZPJE@RD&53SAfeg z4-c^=-K+Er(L`2qJKqO0c+ObcveeiKic6*nb<-K{o|c;nWyw1YyalW0WT(3@>S-0h zciB4!vBz~y8?C1q)qF~4d4kBLM&tnFTG_)0!WK8;4~L1!$8x;%WDVZt3Q5$i0+G$7 zb6gc_pgJLzo!H|5PaK>P*V2b|6qk*olM_Y}aUAdX@}5KU*Pz1SjmI7|Y&P(;Jy!Yq zyZ9}`i7Xc!*`bl9O6_S+Fl}hm@Ac;5oPcvG zm;`JfuW}SuG;?pk^Kx6;vXO3ywNR=hx|j7Wo6r)3ZwSza1(_@Af$?E+je9ghzP8~m zG%T9)c`Nu4?&QI9FC$Z2;^r?N#8)eJu1z$f_>@y(p~K-xPSaAoxzHOvyEa^Kp+Y#u zCBGShit#gb^r5BCK->ojP$j}IN26)Ub6sBasb;}0#ltAuh%^+X%~&C1k8cbCi14yT z%({j*?qmcGTbSfEPrO+yoQxmfa^Dwd!sZ?}X^(;aqgCIp@3SvJVn(>FA|#!X6Q5s# z4Y;dkPm4zkxn}UjrlSMEFPy^I)Y*IRqc!Tmh@SeDM6Dh9EZObMO~<7q z%4RH*ib&a3oBa3NT|D@-C?$&1-VvEeA_CY27TCoCi(L%v$3}?bOk*$zuL8e+YloG< zo`Ak*&MLt)pz{i^0<}?tG!GA8~GM4pd?l!&&kM(SmDA5m4e;fH+A(|tu%G4T)U-cGO%03wb-AA<7pUqEeg^w z_6*f8P_DQ}#ic|T$97P5sMztMb;T))tM_KBK_e6xi zmEm&`fV?0ox*!fAqcs;k(Ai;HsG>?8E2GV$hYY=2Ob~%h66EBpn?n)_ZW)?Lz!{oI z?hBCTls>LdD7RUR=D>CA>j`@Op~KIRA55qkc!$p)jZjx77LEss#$9l47TSUE%N4sC z9w{oa+wK#GuOVhcw;gI0tplFQ(iYtp2+Wu_fSzN5b=uBgyPg;{%9a0 zDF?#cnY#(*P=QDww&ZxvkZ-1Qn@I4?7umTH%z`@n@MtD4GeLx#gqk0?Xsliu{pxrF z|33H<(x~f%5oeR#Hs&X9sq2VoArWFVQ(6nZT#w&&>)RNu?6))6l1}b0rpFf`0-9M{ zZ(p-&He|ChH^xnZL??`(9y<$w>B=Lk(@r|2ccV!U7A?XM zY4I^;M@}#v;Su9P)d#$E7D^O0djTL(VKepmfJVn}ytCX5!S?zNiAp>Xh<`##7n}pc zQHqK)&|yfL`2(AdBl#l%u_2+e7D-q@d(mxQb-MfKUEEtA9g`;${mwx{4GCvsAdad= zx*3YX1Qt3B1>FZ!o=AoRVQxczT+4wh3j?u)3jSp0|Fmi9Pi%A>b1?Y-7q|sD-~NM{ zJE{AhKkhz%QQCi=A}OsL!x#|fbH{B>Wyp=c$#;#Df2^sqOgV}8|D6qT4fkbV21fz^ z@9Z?9``?YN7r_79TRY|ZUr&-)^P6gwd>>X|yd{T@m6&f4(kcwN2xnC$T!gg}BQEdU zr2(`6@C0ABig)x_Wb3WUgi%?c(&_!UYFzF(k0;6uVx~CzxaC)LEG3=EGq@scz z2isj{d)Ig=k~XNQ+Tm459{gt?AIy@Qv!KGxk301C65D@gJ+S?=7QdVs_%14nt{-qD zEnz;3W|br2wI%l}(UGcBx$o^BO+4UF;5&lGo%m|kHP!_BVZ=E3GLWgOB|xw5@t)$L zU9d-Wnv}~R*9%NtOg`ZFuxHKi{Iwpy6m+eCFC_IwZA;%%{!~q6_6nkpl;Ju4q0FOK ziA?=pjpq$)fhtpv*N#5lkku;#5jwp;9s|(Oeh5hv!M|uzGxa)uvrKjQ0IflK=BgY( z0JaEZ>5+eZF&VWz3iM#=Xb^}uuMjESSX~&YDE+xeaRM!jlNbJ6lyp!-2j~kUj#nu_ z->fIcyU8aS{Akw2H2vD^=sz_j?*Dxu6|ms`-}cT6wf_TU5bytO?`}6r{qHGKss5Gf zU#b2rtp3qz1ErIy5DrAjXNX3Gt3`Z?(q%2fa^I_(O7h0WQPk@D{1F;n=^=0u=^8{H`fsNv9ja5MvrU+2R4;H04obdOx>S4F5@qYWK$U*rw5vS?NYlkSxnQyAmOB1I z47$(=NhzmOSakl8wh*$R-^vbh89`X zYaCKdm^)@?Bdc$t>y14*l%b0!alKfyjGxY%y1eui=qbI6jAnZjVK1Xh%{jhp)_KL4 z79}z6@ee|m!d}EFsVbo4Z1M{=d-9TA{J~NckvZv!Ca&OcI1w%BYl?A-roVvoRuO1) z<6|g9Sct46d7U9g$He>?86y%T`X~DsU5Am%ZEhzq43X+2lmL0;2i)SD0QvLjZ#8zH zoXRRuCH2zp=>dhnB<)uO?hLr0gNx>56rUS<`L)KMwLyl7t$_FxpKT;7B!0}H^gH*V zb`!dzsDd0VqE&`0sdnvVV}mhB;$?SekAGTjEkF^ zW91SMT^oZ9JAo_jCH`XFSVspKFJywZafp-1k}saDz2*6#b-h_jHg4795o_|iafub% zA6VlMxHCC0qQ!#(2B~AYK`%Kx^ML2`leV_DhLq^p=pTi2Ct`Sg?uyASPcgiAfa(RF zaJXnC<(HeJ;a>pAC*>zmQcRXAM;s6}9|yEQ3VeF9icVHQNDiib9u`^0xYQu>b$mp7 zBOvcvSU!Nar=&fqvO_chlJ4t|Y70)1g{Or;QbiotwzV;DuwSZ|ZxxqRA_0?ICZh*wKq&KfzHq^@iKivp{Kn zd#KTOraiPI_v}etvsLLk_mI!7dM>c~!qA{-)SrUhzMtAtdo{g9k#~_`lVQY6_!6qu zpXK(PEj+hk>9Oyh!%OqQ%i;VtlB{4PsSF>^`J-6?EXxiB!yC`c%ypTWV3B4$%U96- zsG{0f%Y=&)*GVaGm6R5I5pu=-E6as#Hr=hG_lRSw8^3v#fQZsn+%XBnBSaWcTg}N} zp33lPD^#&4O2TW#dYq!_qdAnzndgz*bP2wYnELh>b9?D?OFmk2;WcUTpNA|eAaOO= zbx#+n`_T9EvCf0rgx-(m;Z*qw<<{f7munzFh#&7xy@p$A7= zE^e}!#bc-9TnzobKZ-{t#ngxWJGuPe8#J`N=hJ-doc%PlN^0=9LscCJ=4n`})Zt^M zi);xo6<~wq-8;B-0fev;^s!Z2HTq*b#~c4ieJdB0+?esLSKihv?>7A*S`VVl8-|Cq z_;Z)Azlb)^ScRV|th^M6gKXTDv#oqtL_rG=_ud(%F_r>n>-L2>3{|_I_^Z%zv7)R<2j(XOj z46Jr=#`V^7g z<4irX95=dt!gMZNuCAT2E~=d3t#~K$b3S=>ND80ybDvOi&iD^@+IcN5kw;g#V`f0q z3J#81EvTcBoA3Iw{(!AN8#r{Qm91}WzGUP3!KS8NboO5#Lp~Ohc{*wkffK?$3jVsH z&;O}b`y-efkvS_`(3`#i3tC zA@RGeY2D+~Gib=Q9s1)CUkSSD8$)Ac1jZdShhqKT`9?)nH7XUtXjHI~Mn!;!qD)`^ zN#E4$J8Oh7{rMM!u49z6SbVbziEhMCT@;2AaSkfq;KOXKb9{bze17$&-RYWts0!8L z9|?Hu{nP<`XjI@n6;S7(-8nWl=-_uFh1~OrzEy;!bz0qH3eJs>*CfmFT_z2zAibu% z(hg59I^E-QbK~s1eUyVh5t_R3YQJ^TRviIE8h?HO4`^P>mj)71z}0@h`XI)6Gokqh zH_kBm0G1%t{z+@UV@-JUTNQNJxsZoR{)tBWE^*^ZTT!be>>LpPHA2=@+(k+?* z%OLq@H$?p%&oK_W$NN-|Ww~tl-A*TiUg?H=WX_n6&k2cmD8`%c| z@*q<5cK*}_e<+~ODO{2OVlb#a1^`n9N1E{j!fjA zsvBHa`Kd^C{jc~Ce5|;G00aZAEvDT5SA6q&;tVvu_s;Hz6qbeq@A~8HM}bkx!EUeu z^zlk)tu|;8i+}CT#_$y&$mp*KF<#)<8;pI&3k`LcmBJx+9Ek>=w~Qr<@AvV=xids_ zaeiX3Ilbu6a|s4(|C0UM0@aJy_sXBx&b9Z8 zJrhF!?59Q?8Yj8-qlQz1W(sF%4<=JI_tkvT);LEeTRaK74h{(bR8>BTH%;R2JrmUQ z44wyDTn|IY>1rSKIC#`gFWY!iK(YX7A6bZPyt`&#DLp5b51%kIoloh>MJ$OZNZ=`c z!m3&hXT>M_kri4%c~j^Gm9NTm*qCf4G%u$HT@Vqp*RmZll-8KayTto2iNO*o4xjjk zH&gF6nu^O&LgO>^TfP=AA=hX*|B8aG2>&NYHUjxSD|QlkxSm-3B9$UnuB)9~z5a}7 z6c^g0ETDy8r(TIN6UapNrv5lGS`Su>C+AJ8+}>p{@xkS=ikiGYW%)v0xd1%n0`PR= z>2JRR6iP~x7GeZo+`snGUYW{NrZSbOOl2xlnaWhAGL@-JWhzsd%2cK@m8nc+DpQ%t URHibO>HjGG7fk7{PyomP0D|Ex-2eap literal 0 HcmV?d00001 diff --git a/ext/bash/mco_completion.sh b/ext/bash/mco_completion.sh new file mode 100644 index 0000000..f37b1d4 --- /dev/null +++ b/ext/bash/mco_completion.sh @@ -0,0 +1,57 @@ +_mco() { + local agents options + + COMPREPLY=() + local cur=${COMP_WORDS[COMP_CWORD]} + local prev=${COMP_WORDS[COMP_CWORD-1]} + + # Where are the plugins? + local libdir=$(sed -n 's@libdir = @@p' /etc/mcollective/client.cfg) + + # All arguments by options + noopt=($(tr ' ' '\n' <<<${COMP_WORDS[@]} | \ + grep -v "^$cur$" | grep -v -- '^-')) + + local count_noopt=${#noopt[@]} + local cmd=${noopt[0]} + local app=${noopt[1]} + + # A bug in the output of --help prevents + # from parsing all options, so we list the common ones here + local common_options="-T --target -c --config --dt --discovery-timeout \ + -t --timeout -q --quiet -v --verbose -h --help -W --with -F \ + --wf --with-fact -C --wc --with-class -A --wa --with-agent -I \ + --wi --with-identity" + + if [ $COMP_CWORD -eq 1 ]; then + apps=$($cmd completion --list-applications) + COMPREPLY=($(compgen -W "$apps" -- "$cur")) + elif [ $COMP_CWORD -gt 1 ]; then + options="${common_options} $($cmd $app --help | grep -o -- '-[^, ]\+')" + + if [ "x${app}" = "xrpc" ]; then + if [[ $count_noopt -eq 2 || "x${prev}" = "x--agent" ]]; then + # Complete with agents + agents=$($cmd completion --list-agents) + options="$options $agents" + elif [[ $count_noopt -eq 3 || "x${prev}" = "x--action" ]]; then + # Complete with agent actions + rpcagent=${noopt[2]} + actions=$($cmd completion --list-actions \ + --agent "$rpcagent") + options="$options $actions" + elif [ $count_noopt -gt 3 ]; then + # Complete with key=value + rpcagent=${noopt[2]} + rpcaction=${noopt[3]} + inputs=$($cmd completion --list-inputs \ + --agent "$rpcagent" --action "$rpcaction") + options="$options $inputs" + fi + fi + + COMPREPLY=($(compgen -W "$options" -S ' ' -- "$cur")) + fi +} +[ -n "${have:-}" ] && complete -o nospace -F _mco mco + diff --git a/ext/debian/compat b/ext/debian/compat new file mode 100644 index 0000000..7f8f011 --- /dev/null +++ b/ext/debian/compat @@ -0,0 +1 @@ +7 diff --git a/ext/debian/control b/ext/debian/control new file mode 100644 index 0000000..7fb71de --- /dev/null +++ b/ext/debian/control @@ -0,0 +1,42 @@ +Source: mcollective +Section: utils +Priority: extra +Maintainer: Riccardo Setti +Build-Depends: debhelper (>= 7), dpatch, cdbs +Standards-Version: 3.8.0 +Homepage: http://marionette-collective.org/ + +Package: mcollective +Architecture: all +Depends: ruby (>= 1.8.1), mcollective-common (= ${source:Version}) +Description: build server orchestration or parallel job execution systems + The Marionette Collective aka. mcollective is a framework + to build server orchestration or parallel job execution systems. + +Package: mcollective-client +Architecture: all +Depends: ruby (>= 1.8.1), mcollective-common (= ${source:Version}) +Description: build server orchestration or parallel job execution systems + The Marionette Collective aka. mcollective is a framework + to build server orchestration or parallel job execution system + +Package: mcollective-common +Replaces: mcollective (<< 2.0.0-1) +Breaks: mcollective (<< 2.0.0-1), mcollective-client (<< 2.0.0-1) +Architecture: all +Depends: ruby (>= 1.8.1) , rubygems +Description: build server orchestration or parallel job execution systems + The Marionette Collective aka. mcollective is a framework + to build server orchestration or parallel job execution systems. + . + Common files for mcollective packages. + +Package: mcollective-doc +Architecture: all +Section: doc +Description: Documentation for mcollective + The Marionette Collective aka. mcollective is a framework + to build server orchestration or parallel job execution systems. + . + Documentation package. + diff --git a/ext/debian/copyright b/ext/debian/copyright new file mode 100644 index 0000000..ec55a9a --- /dev/null +++ b/ext/debian/copyright @@ -0,0 +1,29 @@ +This package was debianized by Riccardo Setti on +Mon, 04 Jan 2010 17:09:50 +0000. + +It was downloaded from http://code.google.com/p/mcollective + +Upstream Author: + R.I.Pienaar + +Copyright: + + Copyright 2009 R.I.Pienaar + +License: + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +The Debian packaging is (C) 2010, Riccardo Setti and +is licensed under the Apache License v2. + diff --git a/ext/debian/mcollective-client.install b/ext/debian/mcollective-client.install new file mode 100644 index 0000000..31b43d3 --- /dev/null +++ b/ext/debian/mcollective-client.install @@ -0,0 +1,6 @@ +usr/bin/mco usr/bin/ +usr/sbin/mc-* usr/sbin/ +etc/mcollective/client.cfg etc/mcollective +usr/share/mcollective/plugins/mcollective/application usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/aggregate usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/pluginpackager usr/share/mcollective/plugins/mcollective diff --git a/ext/debian/mcollective-common.install b/ext/debian/mcollective-common.install new file mode 100644 index 0000000..058fa88 --- /dev/null +++ b/ext/debian/mcollective-common.install @@ -0,0 +1,11 @@ +usr/lib/ruby/1.8/* usr/lib/ruby/1.8/ +etc/mcollective/*.erb etc/mcollective +usr/share/mcollective/plugins/mcollective/agent usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/audit usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/connector usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/data usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/facts usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/registration usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/security usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/validator usr/share/mcollective/plugins/mcollective +usr/share/mcollective/plugins/mcollective/discovery usr/share/mcollective/plugins/mcollective diff --git a/ext/debian/mcollective-doc.install b/ext/debian/mcollective-doc.install new file mode 100644 index 0000000..0bd8547 --- /dev/null +++ b/ext/debian/mcollective-doc.install @@ -0,0 +1 @@ +usr/share/doc/mcollective/* usr/share/doc/mcollective-doc/ diff --git a/ext/debian/mcollective.init b/ext/debian/mcollective.init new file mode 100755 index 0000000..f599f4a --- /dev/null +++ b/ext/debian/mcollective.init @@ -0,0 +1,85 @@ +#!/bin/sh +# +# mcollective Application Server for STOMP based agents +# +# +# description: mcollective lets you build powerful Stomp compatible middleware clients in ruby without having to worry too +# much about all the setup and management of a Stomp connection, it also provides stats, logging and so forth +# as a bonus. +# +### BEGIN INIT INFO +# Provides: mcollective +# Required-Start: $remote_fs +# Required-Stop: $remote_fs +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Start daemon at boot time +# Description: Enable service provided by mcollective. +### END INIT INFO + +# check permissions + +uid=`id -u` +[ $uid -gt 0 ] && { echo "You need to be root to run file" ; exit 4 ; } + + +# PID directory +pidfile="/var/run/mcollectived.pid" + +name="mcollective" +mcollectived=/usr/sbin/mcollectived +daemonopts="--pid=${pidfile} --config=/etc/mcollective/server.cfg" + + +# Source function library. +. /lib/lsb/init-functions + +# Check that binary exists +if ! [ -f $mcollectived ] +then + echo "mcollectived binary not found" + exit 5 +fi + +# create pid file if it does not exist +[ ! -f ${pidfile} ] && { touch ${pidfile} ; } + +# See how we were called. +case "$1" in + start) + echo "Starting daemon: " $name + # start the program + start-stop-daemon -S -p ${pidfile} --oknodo -q -a ${mcollectived} -- ${daemonopts} + [ $? = 0 ] && { exit 0 ; } || { exit 1 ; } + log_success_msg "mcollective started" + touch $lock + ;; + stop) + echo "Stopping daemon: " $name + start-stop-daemon -K -R 5 -s "TERM" --oknodo -q -p ${pidfile} + [ $? = 0 ] && { exit 0 ; } || { exit 1 ; } + log_success_msg "mcollective stopped" + ;; + restart) + echo "Restarting daemon: " $name + $0 stop + sleep 2 + $0 start + [ $? = 0 ] && { echo "mcollective restarted" ; exit 0 ; } + ;; + condrestart) + if [ -f $lock ]; then + $0 stop + # avoid race + sleep 2 + $0 start + fi + ;; + status) + status_of_proc -p ${pidfile} ${mcollectived} ${name} && exit 0 || exit $? + ;; + *) + echo "Usage: mcollectived {start|stop|restart|condrestart|status}" + exit 2 + ;; +esac diff --git a/ext/debian/mcollective.install b/ext/debian/mcollective.install new file mode 100644 index 0000000..64e7158 --- /dev/null +++ b/ext/debian/mcollective.install @@ -0,0 +1,5 @@ +usr/sbin/mcollectived usr/sbin +etc/mcollective/facts.yaml etc/mcollective +etc/mcollective/server.cfg etc/mcollective +etc/init.d etc/ +etc/mcollective/ssl diff --git a/ext/debian/patches/00list b/ext/debian/patches/00list new file mode 100644 index 0000000..f356934 --- /dev/null +++ b/ext/debian/patches/00list @@ -0,0 +1 @@ +pluginsdir.dpatch diff --git a/ext/debian/patches/conffile.dpatch b/ext/debian/patches/conffile.dpatch new file mode 100755 index 0000000..07ba476 --- /dev/null +++ b/ext/debian/patches/conffile.dpatch @@ -0,0 +1,115 @@ +#! /bin/sh /usr/share/dpatch/dpatch-run +## conffile.dpatch by +## +## All lines beginning with `## DP:' are a description of the patch. +## DP: fix plugins dir + +@DPATCH@ +diff -urNad mcollective-1.1.4~/etc/client.cfg.dist mcollective-1.1.4/etc/client.cfg.dist +--- mcollective-1.1.4~/etc/client.cfg.dist 2011-04-06 14:13:08.829462165 -0700 ++++ mcollective-1.1.4/etc/client.cfg.dist 2011-04-06 14:12:53.129384114 -0700 +@@ -1,7 +1,7 @@ + topicprefix = /topic/ + main_collective = mcollective + collectives = mcollective +-libdir = /usr/libexec/mcollective ++libdir = /usr/share/mcollective/plugins + logger_type = console + loglevel = warn + +diff -urNad mcollective-1.1.4~/etc/server.cfg.dist mcollective-1.1.4/etc/server.cfg.dist +--- mcollective-1.1.4~/etc/server.cfg.dist 2011-04-06 14:12:30.889527230 -0700 ++++ mcollective-1.1.4/etc/server.cfg.dist 2011-04-06 14:12:23.779407065 -0700 +@@ -1,7 +1,7 @@ + topicprefix = /topic/ + main_collective = mcollective + collectives = mcollective +-libdir = /usr/libexec/mcollective ++libdir = /usr/share/mcollective/plugins + logfile = /var/log/mcollective.log + loglevel = info + daemonize = 1 + diff --git a/ext/debian/rules b/ext/debian/rules new file mode 100755 index 0000000..2551380 --- /dev/null +++ b/ext/debian/rules @@ -0,0 +1,18 @@ +#!/usr/bin/make -f + +DEB_MAKE_CLEAN_TARGET := +DEB_MAKE_INSTALL_TARGET := install DESTDIR=$(CURDIR)/debian/tmp + +include /usr/share/cdbs/1/rules/debhelper.mk +include /usr/share/cdbs/1/rules/dpatch.mk +include /usr/share/cdbs/1/class/makefile.mk +DEB_MAKE_INVOKE = $(DEB_MAKE_ENVVARS) make -f ext/Makefile -C $(DEB_BUILDDIR) + +install/mcollective:: + mv $(CURDIR)/debian/tmp/etc/mcollective/server.cfg.dist $(CURDIR)/debian/tmp/etc/mcollective/server.cfg + mv $(CURDIR)/debian/tmp/etc/mcollective/client.cfg.dist $(CURDIR)/debian/tmp/etc/mcollective/client.cfg + mv $(CURDIR)/debian/tmp/etc/mcollective/facts.yaml.dist $(CURDIR)/debian/tmp/etc/mcollective/facts.yaml +# dh_installinit -pmcollective -o + +binary-fixup/mcollective:: + chmod 640 $(CURDIR)/debian/mcollective/etc/mcollective/server.cfg diff --git a/ext/help-templates/README b/ext/help-templates/README new file mode 100644 index 0000000..0b60383 --- /dev/null +++ b/ext/help-templates/README @@ -0,0 +1 @@ +A number of templates for the SimpleRPC DDL based help system diff --git a/ext/help-templates/rpc-help-markdown.erb b/ext/help-templates/rpc-help-markdown.erb new file mode 100644 index 0000000..39caf2b --- /dev/null +++ b/ext/help-templates/rpc-help-markdown.erb @@ -0,0 +1,49 @@ +<%= meta[:name].upcase %> AGENT +<% (meta[:name].size + 7).times do %>=<% end %> + +<%= meta[:description] %> + + Author: <%= meta[:author] %> + Version: <%= meta[:version] %> + License: <%= meta[:license] %> + Timeout: <%= meta[:timeout] %> + Home Page: <%= meta[:url] %> + + + +ACTIONS: +======== +% actions.keys.sort.each do |action| + * <%= action %> +% end + +% actions.keys.sort.each do |action| +_<%= action %>_ action: +<% (action.size + 8).times do %>-<% end %> +<%= actions[action][:description] %> + +% if actions[action][:input].keys.size > 0 + INPUT: +% end +% actions[action][:input].keys.sort.each do |input| + <%= input %>: + Description: <%= actions[action][:input][input][:description] %> + Prompt: <%= actions[action][:input][input][:prompt] %> + Type: <%= actions[action][:input][input][:type] %> +% if actions[action][:input][input][:type] == :string + Validation: <%= actions[action][:input][input][:validation] %> + Length: <%= actions[action][:input][input][:maxlength] %> +% elsif actions[action][:input][input][:type] == :list + Valid Values: <%= actions[action][:input][input][:list].join(", ") %> +% end + +% end + + OUTPUT: +% actions[action][:output].keys.sort.each do |output| + <%= output %>: + Description: <%= actions[action][:output][output][:description] %> + Display As: <%= actions[action][:output][output][:display_as] %> + +% end +% end diff --git a/ext/mc-irb b/ext/mc-irb new file mode 100755 index 0000000..63e7516 --- /dev/null +++ b/ext/mc-irb @@ -0,0 +1,252 @@ +#!/usr/bin/env ruby + +# Simple IRB shell for mcollective +# +# mc-irb nrpe +# Determining the amount of hosts matching filter for 2 seconds .... 47 +# >> rpc :runcommand, :command => "check_disks" +# +# * [ ============================================================> ] 47 / 47 +# +# +# dev1.your.net Request Aborted +# CRITICAL +# Output: DISK CRITICAL - free space: / 176 MB (4% inode=86%); +# Exit Code: 2 +# Performance Data: /=3959MB;3706;3924;0;4361 /boot=26MB;83;88;0;98 /dev/shm=0MB;217;230;0;256 +# +# => true +# >> mchelp +# +# => true +# >> rpc(:runcommand, :command => "check_disks") do |resp| +# ?> puts resp[:sender] + ": " + resp[:data][:output] +# >> end +# +# * [ ============================================================> ] 47 / 47 +# +# dev1.your.net: DISK OK +# +# => true +# >> +# +# You can access the agent variable via @agent from where you can do the usual manipulation of filters etc, +# if you wish to switch to a different agent mid run just do newagent("some_other_agent") +# +# If you install the Bond gem you'll get some DDL assisted completion in the rpc method +require 'rubygems' +require 'irb' + +def consolize &block + yield + + IRB.setup(nil) + irb = IRB::Irb.new + IRB.conf[:MAIN_CONTEXT] = irb.context + irb.context.evaluate("require 'irb/completion'", 0) + + begin + require 'bond' + Bond.start + + Bond.complete(:method => "rpc") do |e| + begin + if e.argument == 1 + if e.arguments.last == "?" + puts "\n\nActions for #{@agent_name}:\n" + + @agent.ddl.actions.each do |action| + puts "%20s - %s" % [ ":#{action}", @agent.ddl.action_interface(action)[:description] ] + end + + print "\n" + e.line + end + + @agent.ddl.actions + + elsif e.argument > 1 + action = eval(e.arguments[0]).to_s + ddl = @agent.ddl.action_interface(action) + + if e.arguments.last == "?" + puts "\n\nArguments for #{action}:\n" + ddl[:input].keys.each do |input| + puts "%20s - %s" % [ ":#{input}", ddl[:input][input][:description] ] + end + + print "\n" + e.line + end + + [ddl[:input].keys, :verbose].flatten + end + rescue Exception + [] + end + end + rescue Exception + end + + trap("SIGINT") do + irb.signal_handle + end + catch(:IRB_EXIT) do + irb.eval_input + end +end + +def mchelp + system("mc-rpc --agent-help #{@agent_name}|less") + true +end + +def rpc(method_name, *args, &block) + unless block_given? + if args.size > 0 + args = args.first + else + args = {} + end + + if args[:verbose] + args.delete(:verbose) + + printrpc(@agent.send(method_name, args), :verbose => true) + printrpcstats + else + printrpc @agent.send(method_name, args) + printrpcstats + end + + else + @agent.send(method_name, args.first).each do |resp| + yield resp + end + + printrpcstats + end + + true +rescue MCollective::DDLValidationError => e + puts "Request did not pass DDL validation: #{e}" +end + +def print_filter + puts "Active Filter matched #{discover.size} hosts:" + puts "\tIdentity: #{@agent.filter['identity'].pretty_inspect}" + puts "\t Classes: #{@agent.filter['cf_class'].pretty_inspect}" + puts "\t Facts: #{@agent.filter['fact'].pretty_inspect}" + puts "\t Agents: #{@agent.filter['agent'].pretty_inspect}" + + discover.size > 0 ? true : false +end + +def newagent(agent) + @agent_name = agent + + @options[:filter]["agent"] = [] + @agent = rpcclient(@agent_name, :options => @options) + + discover + + @agent.progress = true + + print_filter +end + +def identity_filter(*args) + @agent.identity_filter(*args) + + print_filter +end + +def fact_filter(*args) + @agent.fact_filter(*args) + + print_filter +end + +def agent_filter(*args) + @agent.agent_filter(*args) + + print_filter +end + +def class_filter(*args) + @agent.class_filter(*args) + + print_filter +end + +def reset_filter + @agent.reset_filter + + print_filter +end + +def reset + @agent.reset + + print_filter +end + +def discover + @agent.discover +end + +def mc? + puts < for a list of actions or + arguments, do simple : to get completion on action names and + arguments without description of each + +EOF + true +end + +consolize do + require 'mcollective' + + + include MCollective::RPC + + @options = rpcoptions + + unless ARGV.size == 1 + puts "Please specify an agent name on the command line" + exit 1 + end + + puts "The Marionette Collective Interactive Ruby Shell version #{MCollective.version}" + + puts + newagent(ARGV[0]) + puts + puts "Use mc? to get help on using this shell" +end diff --git a/ext/mc-rpc-restserver.rb b/ext/mc-rpc-restserver.rb new file mode 100755 index 0000000..1ba68a2 --- /dev/null +++ b/ext/mc-rpc-restserver.rb @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby + +# A very simple demonstration of writing a REST server +# for Simple RPC clients that takes requests over HTTP +# and returns results as JSON structures. + +require 'rubygems' +require 'sinatra' +require 'mcollective' +require 'json' + +include MCollective::RPC + +# http:///mcollective/rpctest/echo/msg=hello%20world +# +# Creates a new Simple RPC client for the 'rpctest' agent, calls +# the echo action with a message 'hello world'. +# +# Returns all the answers as a JSON data block +get '/mcollective/:agent/:action/*' do + mc = rpcclient(params[:agent]) + mc.discover + + arguments = {} + + # split up the wildcard params into key=val pairs and + # build the arguments hash + params[:splat].each do |arg| + arguments[$1.to_sym] = $2 if arg =~ /^(.+?)=(.+)$/ + end + + JSON.dump(mc.send(params[:action], arguments).map{|r| r.results}) +end + diff --git a/ext/openbsd/README b/ext/openbsd/README new file mode 100644 index 0000000..4467d1d --- /dev/null +++ b/ext/openbsd/README @@ -0,0 +1,4 @@ +These files are meant to be places in /usr/ports/mystuff/sysutils/ and then follow +the OpenBSD guidelines for "custom" ports. + +Happy hacking diff --git a/ext/openbsd/port-files/mcollective/Makefile b/ext/openbsd/port-files/mcollective/Makefile new file mode 100644 index 0000000..5bd6f48 --- /dev/null +++ b/ext/openbsd/port-files/mcollective/Makefile @@ -0,0 +1,28 @@ +PKG_ARCH= * +COMMENT= The Marionette Collective + +DISTNAME= mcollective-2.2.1 + +CATEGORIES= sysutils + +HOMEPAGE= http://puppetlabs.com/mcollective/ +MASTER_SITES= http://puppetlabs.com/downloads/mcollective/ +EXTRACT_SUFX= .tgz + +# GFDL +PERMIT_PACKAGE_CDROM= Yes +PERMIT_PACKAGE_FTP= Yes +PERMIT_DISTFILES_CDROM= Yes +PERMIT_DISTFILES_FTP= Yes + +NO_BUILD= Yes +NO_REGRESS= Yes + +# makefile is in ext/ +MAKE_FILE=ext/Makefile + +post-patch: + ${SUBST_CMD} ${WRKSRC}/etc/server.cfg.dist + rm -f ${WRKSRC}/etc/server.cfg.dist.{orig,beforesubst} + +.include diff --git a/ext/openbsd/port-files/mcollective/distinfo b/ext/openbsd/port-files/mcollective/distinfo new file mode 100644 index 0000000..b351977 --- /dev/null +++ b/ext/openbsd/port-files/mcollective/distinfo @@ -0,0 +1,5 @@ +MD5 (mcollective-2.2.1.tgz) = 82t1Sk+HRSXR9L4x4eTaSg== +RMD160 (mcollective-2.2.1.tgz) = PMTcw0VMqrr7rSOYApCW0q8SXok= +SHA1 (mcollective-2.2.1.tgz) = 1yPH0CgmQa5J4VeiDEAExNbn3K4= +SHA256 (mcollective-2.2.1.tgz) = 2KaZk2KrMXPJ7aycM+Uy+0E8hyUsONoP9HkdjpAZ4YM= +SIZE (mcollective-2.2.1.tgz) = 1123400 diff --git a/ext/openbsd/port-files/mcollective/patches/patch-etc_server_cfg_dist b/ext/openbsd/port-files/mcollective/patches/patch-etc_server_cfg_dist new file mode 100644 index 0000000..61bf822 --- /dev/null +++ b/ext/openbsd/port-files/mcollective/patches/patch-etc_server_cfg_dist @@ -0,0 +1,10 @@ +$OpenBSD$ +--- etc/server.cfg.dist.orig Thu Jun 24 15:57:17 2010 ++++ etc/server.cfg.dist Thu Jun 24 15:57:25 2010 +@@ -1,5 +1,5 @@ + topicprefix = /topic/mcollective +-libdir = /usr/libexec/mcollective ++libdir = ${PREFIX}/share/mcollective/plugins + logfile = /var/log/mcollective.log + loglevel = info + daemonize = 1 diff --git a/ext/openbsd/port-files/mcollective/patches/patch-ext_Makefile b/ext/openbsd/port-files/mcollective/patches/patch-ext_Makefile new file mode 100644 index 0000000..26e2616 --- /dev/null +++ b/ext/openbsd/port-files/mcollective/patches/patch-ext_Makefile @@ -0,0 +1,67 @@ +diff --git ext/Makefile ext/Makefile +index 029fda4..638d7a5 100644 +--- ext/Makefile ++++ ext/Makefile +@@ -1,6 +1,5 @@ + #!/usr/bin/make -f + +-DESTDIR= + + build: + +@@ -9,36 +8,34 @@ clean: + install: install-bin install-lib install-conf install-plugins install-doc + + install-bin: +- install -d $(DESTDIR)/usr/sbin +- install -d $(DESTDIR)/usr/bin +- cp bin/mc-* $(DESTDIR)/usr/sbin +- cp bin/mco $(DESTDIR)/usr/bin +- cp bin/mcollectived $(DESTDIR)/usr/sbin/mcollectived ++ install -d $(PREFIX)/sbin ++ install -d $(PREFIX)/bin ++ cp bin/mc-* $(PREFIX)/sbin ++ cp bin/mco $(PREFIX)/bin ++ cp bin/mcollectived $(PREFIX)/sbin/mcollectived + + install-lib: +- install -d $(DESTDIR)/usr/lib/ruby/1.8/ +- cp -a lib/* $(DESTDIR)/usr/lib/ruby/1.8/ ++ install -d $(PREFIX)/lib/ruby/1.8/ ++ cp -R lib/* $(PREFIX)/lib/ruby/1.8/ + + install-conf: +- install -d $(DESTDIR)/etc/mcollective/ +- install -d $(DESTDIR)/etc/init.d +- cp -r etc/* $(DESTDIR)/etc/mcollective/ +- cp mcollective.init $(DESTDIR)/etc/init.d/mcollective +- rm $(DESTDIR)/etc/mcollective/ssl/PLACEHOLDER +- rm $(DESTDIR)/etc/mcollective/ssl/clients/PLACEHOLDER ++ install -d $(PREFIX)/share/examples/mcollective/ ++ cp -R etc/* $(PREFIX)/share/examples/mcollective/ ++ rm $(PREFIX)/share/examples/mcollective/ssl/PLACEHOLDER ++ rm $(PREFIX)/share/examples/mcollective/ssl/clients/PLACEHOLDER + + install-plugins: +- install -d $(DESTDIR)/usr/share/mcollective/ +- cp -a plugins $(DESTDIR)/usr/share/mcollective/ ++ install -d $(PREFIX)/share/mcollective/ ++ cp -R plugins $(PREFIX)/share/mcollective/ + + install-doc: +- install -d $(DESTDIR)/usr/share/doc/ +- cp -a doc $(DESTDIR)/usr/share/doc/mcollective ++ install -d $(PREFIX)/share/doc/ ++ cp -R doc $(PREFIX)/share/doc/mcollective + + uninstall: +- rm -f $(DESTDIR)/usr/sbin/mcollectived +- rm -rf $(DESTDIR)/usr/lib/ruby/1.8/mcollective* +- rm -rf $(DESTDIR)/usr/share/mcollective +- rm -rf $(DESTDIR)/etc/mcollective ++ rm -f $(PREFIX)/sbin/mcollectived ++ rm -rf $(PREFIX)/lib/ruby/1.8/mcollective* ++ rm -rf $(PREFIX)/share/mcollective ++ rm -rf $(PREFIX)/share/examples/mcollective + + .PHONY: build clean install uninstall diff --git a/ext/openbsd/port-files/mcollective/pkg/DESCR b/ext/openbsd/port-files/mcollective/pkg/DESCR new file mode 100644 index 0000000..5ef48d0 --- /dev/null +++ b/ext/openbsd/port-files/mcollective/pkg/DESCR @@ -0,0 +1,2 @@ +The Marionette Collective aka. mcollective is a framework to +build server orchestration or parallel job execution systems. diff --git a/ext/openbsd/port-files/mcollective/pkg/MESSAGE b/ext/openbsd/port-files/mcollective/pkg/MESSAGE new file mode 100644 index 0000000..89fb014 --- /dev/null +++ b/ext/openbsd/port-files/mcollective/pkg/MESSAGE @@ -0,0 +1,3 @@ +If you wish to have mcollective started automatically at boot time, +update your pkg_scripts variable, e.g. in /etc/rc.conf.local. +See rc.d(8) for details. diff --git a/ext/openbsd/port-files/mcollective/pkg/PLIST b/ext/openbsd/port-files/mcollective/pkg/PLIST new file mode 100644 index 0000000..bbe0dd8 --- /dev/null +++ b/ext/openbsd/port-files/mcollective/pkg/PLIST @@ -0,0 +1,600 @@ +@comment $OpenBSD$ +bin/mco +lib/ruby/ +lib/ruby/1.8/ +lib/ruby/1.8/mcollective/ +lib/ruby/1.8/mcollective.rb +lib/ruby/1.8/mcollective/agent.rb +lib/ruby/1.8/mcollective/agents.rb +lib/ruby/1.8/mcollective/aggregate/ +lib/ruby/1.8/mcollective/aggregate.rb +lib/ruby/1.8/mcollective/aggregate/base.rb +lib/ruby/1.8/mcollective/aggregate/result/ +lib/ruby/1.8/mcollective/aggregate/result.rb +lib/ruby/1.8/mcollective/aggregate/result/base.rb +lib/ruby/1.8/mcollective/aggregate/result/collection_result.rb +lib/ruby/1.8/mcollective/aggregate/result/numeric_result.rb +lib/ruby/1.8/mcollective/application.rb +lib/ruby/1.8/mcollective/applications.rb +lib/ruby/1.8/mcollective/cache.rb +lib/ruby/1.8/mcollective/client.rb +lib/ruby/1.8/mcollective/config.rb +lib/ruby/1.8/mcollective/connector/ +lib/ruby/1.8/mcollective/connector.rb +lib/ruby/1.8/mcollective/connector/base.rb +lib/ruby/1.8/mcollective/data/ +lib/ruby/1.8/mcollective/data.rb +lib/ruby/1.8/mcollective/data/base.rb +lib/ruby/1.8/mcollective/data/result.rb +lib/ruby/1.8/mcollective/ddl/ +lib/ruby/1.8/mcollective/ddl.rb +lib/ruby/1.8/mcollective/ddl/agentddl.rb +lib/ruby/1.8/mcollective/ddl/base.rb +lib/ruby/1.8/mcollective/ddl/dataddl.rb +lib/ruby/1.8/mcollective/ddl/discoveryddl.rb +lib/ruby/1.8/mcollective/ddl/validatorddl.rb +lib/ruby/1.8/mcollective/discovery.rb +lib/ruby/1.8/mcollective/facts/ +lib/ruby/1.8/mcollective/facts.rb +lib/ruby/1.8/mcollective/facts/base.rb +lib/ruby/1.8/mcollective/generators/ +lib/ruby/1.8/mcollective/generators.rb +lib/ruby/1.8/mcollective/generators/agent_generator.rb +lib/ruby/1.8/mcollective/generators/base.rb +lib/ruby/1.8/mcollective/generators/data_generator.rb +lib/ruby/1.8/mcollective/generators/templates/ +lib/ruby/1.8/mcollective/generators/templates/action_snippet.erb +lib/ruby/1.8/mcollective/generators/templates/data_input_snippet.erb +lib/ruby/1.8/mcollective/generators/templates/ddl.erb +lib/ruby/1.8/mcollective/generators/templates/plugin.erb +lib/ruby/1.8/mcollective/log.rb +lib/ruby/1.8/mcollective/logger/ +lib/ruby/1.8/mcollective/logger.rb +lib/ruby/1.8/mcollective/logger/base.rb +lib/ruby/1.8/mcollective/logger/console_logger.rb +lib/ruby/1.8/mcollective/logger/file_logger.rb +lib/ruby/1.8/mcollective/logger/syslog_logger.rb +lib/ruby/1.8/mcollective/matcher/ +lib/ruby/1.8/mcollective/matcher.rb +lib/ruby/1.8/mcollective/matcher/parser.rb +lib/ruby/1.8/mcollective/matcher/scanner.rb +lib/ruby/1.8/mcollective/message.rb +lib/ruby/1.8/mcollective/monkey_patches.rb +lib/ruby/1.8/mcollective/optionparser.rb +lib/ruby/1.8/mcollective/pluginmanager.rb +lib/ruby/1.8/mcollective/pluginpackager/ +lib/ruby/1.8/mcollective/pluginpackager.rb +lib/ruby/1.8/mcollective/pluginpackager/agent_definition.rb +lib/ruby/1.8/mcollective/pluginpackager/standard_definition.rb +lib/ruby/1.8/mcollective/registration/ +lib/ruby/1.8/mcollective/registration.rb +lib/ruby/1.8/mcollective/registration/base.rb +lib/ruby/1.8/mcollective/rpc/ +lib/ruby/1.8/mcollective/rpc.rb +lib/ruby/1.8/mcollective/rpc/actionrunner.rb +lib/ruby/1.8/mcollective/rpc/agent.rb +lib/ruby/1.8/mcollective/rpc/audit.rb +lib/ruby/1.8/mcollective/rpc/client.rb +lib/ruby/1.8/mcollective/rpc/helpers.rb +lib/ruby/1.8/mcollective/rpc/progress.rb +lib/ruby/1.8/mcollective/rpc/reply.rb +lib/ruby/1.8/mcollective/rpc/request.rb +lib/ruby/1.8/mcollective/rpc/result.rb +lib/ruby/1.8/mcollective/rpc/stats.rb +lib/ruby/1.8/mcollective/runner.rb +lib/ruby/1.8/mcollective/runnerstats.rb +lib/ruby/1.8/mcollective/security/ +lib/ruby/1.8/mcollective/security.rb +lib/ruby/1.8/mcollective/security/base.rb +lib/ruby/1.8/mcollective/shell.rb +lib/ruby/1.8/mcollective/ssl.rb +lib/ruby/1.8/mcollective/unix_daemon.rb +lib/ruby/1.8/mcollective/util.rb +lib/ruby/1.8/mcollective/validator.rb +lib/ruby/1.8/mcollective/vendor/ +lib/ruby/1.8/mcollective/vendor.rb +lib/ruby/1.8/mcollective/vendor/json/ +lib/ruby/1.8/mcollective/vendor/json/.gitignore +lib/ruby/1.8/mcollective/vendor/json/CHANGES +lib/ruby/1.8/mcollective/vendor/json/COPYING +lib/ruby/1.8/mcollective/vendor/json/COPYING-json-jruby +lib/ruby/1.8/mcollective/vendor/json/GPL +lib/ruby/1.8/mcollective/vendor/json/README +lib/ruby/1.8/mcollective/vendor/json/README-json-jruby.markdown +lib/ruby/1.8/mcollective/vendor/json/Rakefile +lib/ruby/1.8/mcollective/vendor/json/TODO +lib/ruby/1.8/mcollective/vendor/json/VERSION +lib/ruby/1.8/mcollective/vendor/json/benchmarks/ +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data/ +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/.keep +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkComparison.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_fast.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_pretty.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt#generator_safe.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkExt.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_fast.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_pretty.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure#generator_safe.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkPure.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails#generator.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/GeneratorBenchmarkRails.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkComparison.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt#parser.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkExt.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure#parser.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkPure.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails#parser.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkRails.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser-autocorrelation.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML#parser.dat +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data-p4-3GHz-ruby18/ParserBenchmarkYAML.log +lib/ruby/1.8/mcollective/vendor/json/benchmarks/data/.keep +lib/ruby/1.8/mcollective/vendor/json/benchmarks/generator2_benchmark.rb +lib/ruby/1.8/mcollective/vendor/json/benchmarks/generator_benchmark.rb +lib/ruby/1.8/mcollective/vendor/json/benchmarks/ohai.json +lib/ruby/1.8/mcollective/vendor/json/benchmarks/ohai.ruby +lib/ruby/1.8/mcollective/vendor/json/benchmarks/parser2_benchmark.rb +lib/ruby/1.8/mcollective/vendor/json/benchmarks/parser_benchmark.rb +lib/ruby/1.8/mcollective/vendor/json/bin/ +lib/ruby/1.8/mcollective/vendor/json/bin/edit_json.rb +lib/ruby/1.8/mcollective/vendor/json/bin/prettify_json.rb +lib/ruby/1.8/mcollective/vendor/json/data/ +lib/ruby/1.8/mcollective/vendor/json/data/example.json +lib/ruby/1.8/mcollective/vendor/json/data/index.html +lib/ruby/1.8/mcollective/vendor/json/data/prototype.js +lib/ruby/1.8/mcollective/vendor/json/diagrams/ +lib/ruby/1.8/mcollective/vendor/json/diagrams/.keep +lib/ruby/1.8/mcollective/vendor/json/ext/ +lib/ruby/1.8/mcollective/vendor/json/ext/json/ +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/ +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/generator/ +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/generator/extconf.rb +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/generator/generator.c +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/generator/generator.h +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/parser/ +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/parser/extconf.rb +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/parser/parser.c +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/parser/parser.h +lib/ruby/1.8/mcollective/vendor/json/ext/json/ext/parser/parser.rl +lib/ruby/1.8/mcollective/vendor/json/install.rb +lib/ruby/1.8/mcollective/vendor/json/java/ +lib/ruby/1.8/mcollective/vendor/json/java/lib/ +lib/ruby/1.8/mcollective/vendor/json/java/lib/bytelist-1.0.6.jar +lib/ruby/1.8/mcollective/vendor/json/java/lib/jcodings.jar +lib/ruby/1.8/mcollective/vendor/json/java/src/ +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/ +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/ByteListTranscoder.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/Generator.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/GeneratorMethods.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/GeneratorService.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/GeneratorState.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/OptionsReader.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/Parser.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/Parser.rl +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/ParserService.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/RuntimeInfo.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/StringDecoder.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/StringEncoder.java +lib/ruby/1.8/mcollective/vendor/json/java/src/json/ext/Utils.java +lib/ruby/1.8/mcollective/vendor/json/json-java.gemspec +lib/ruby/1.8/mcollective/vendor/json/lib/ +lib/ruby/1.8/mcollective/vendor/json/lib/json/ +lib/ruby/1.8/mcollective/vendor/json/lib/json.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/Array.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/FalseClass.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/Hash.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/Key.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/NilClass.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/Numeric.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/String.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/TrueClass.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/add/ +lib/ruby/1.8/mcollective/vendor/json/lib/json/add/core.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/add/rails.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/common.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/editor.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/ext/ +lib/ruby/1.8/mcollective/vendor/json/lib/json/ext.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/ext/.keep +lib/ruby/1.8/mcollective/vendor/json/lib/json/json.xpm +lib/ruby/1.8/mcollective/vendor/json/lib/json/pure/ +lib/ruby/1.8/mcollective/vendor/json/lib/json/pure.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/pure/generator.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/pure/parser.rb +lib/ruby/1.8/mcollective/vendor/json/lib/json/version.rb +lib/ruby/1.8/mcollective/vendor/json/tests/ +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/ +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail1.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail10.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail11.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail12.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail13.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail14.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail18.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail19.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail2.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail20.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail21.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail22.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail23.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail24.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail25.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail27.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail28.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail3.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail4.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail5.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail6.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail7.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail8.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/fail9.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/pass1.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/pass15.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/pass16.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/pass17.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/pass2.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/pass26.json +lib/ruby/1.8/mcollective/vendor/json/tests/fixtures/pass3.json +lib/ruby/1.8/mcollective/vendor/json/tests/setup_variant.rb +lib/ruby/1.8/mcollective/vendor/json/tests/test_json.rb +lib/ruby/1.8/mcollective/vendor/json/tests/test_json_addition.rb +lib/ruby/1.8/mcollective/vendor/json/tests/test_json_encoding.rb +lib/ruby/1.8/mcollective/vendor/json/tests/test_json_fixtures.rb +lib/ruby/1.8/mcollective/vendor/json/tests/test_json_generate.rb +lib/ruby/1.8/mcollective/vendor/json/tests/test_json_string_matching.rb +lib/ruby/1.8/mcollective/vendor/json/tests/test_json_unicode.rb +lib/ruby/1.8/mcollective/vendor/json/tools/ +lib/ruby/1.8/mcollective/vendor/json/tools/fuzz.rb +lib/ruby/1.8/mcollective/vendor/json/tools/server.rb +lib/ruby/1.8/mcollective/vendor/load_json.rb +lib/ruby/1.8/mcollective/vendor/load_systemu.rb +lib/ruby/1.8/mcollective/vendor/require_vendored.rb +lib/ruby/1.8/mcollective/vendor/systemu/ +lib/ruby/1.8/mcollective/vendor/systemu/LICENSE +lib/ruby/1.8/mcollective/vendor/systemu/README +lib/ruby/1.8/mcollective/vendor/systemu/README.erb +lib/ruby/1.8/mcollective/vendor/systemu/Rakefile +lib/ruby/1.8/mcollective/vendor/systemu/lib/ +lib/ruby/1.8/mcollective/vendor/systemu/lib/systemu.rb +lib/ruby/1.8/mcollective/vendor/systemu/samples/ +lib/ruby/1.8/mcollective/vendor/systemu/samples/a.rb +lib/ruby/1.8/mcollective/vendor/systemu/samples/b.rb +lib/ruby/1.8/mcollective/vendor/systemu/samples/c.rb +lib/ruby/1.8/mcollective/vendor/systemu/samples/d.rb +lib/ruby/1.8/mcollective/vendor/systemu/samples/e.rb +lib/ruby/1.8/mcollective/vendor/systemu/samples/f.rb +lib/ruby/1.8/mcollective/vendor/systemu/systemu.gemspec +lib/ruby/1.8/mcollective/windows_daemon.rb +sbin/mc-call-agent +sbin/mcollectived +share/doc/mcollective/ +share/doc/mcollective/Array.html +share/doc/mcollective/COPYING.html +share/doc/mcollective/Dir.html +share/doc/mcollective/MCollective/ +share/doc/mcollective/MCollective.html +share/doc/mcollective/MCollective/Agent.html +share/doc/mcollective/MCollective/Agents.html +share/doc/mcollective/MCollective/Aggregate/ +share/doc/mcollective/MCollective/Aggregate.html +share/doc/mcollective/MCollective/Aggregate/Base.html +share/doc/mcollective/MCollective/Aggregate/Result/ +share/doc/mcollective/MCollective/Aggregate/Result.html +share/doc/mcollective/MCollective/Aggregate/Result/Base.html +share/doc/mcollective/MCollective/Aggregate/Result/CollectionResult.html +share/doc/mcollective/MCollective/Aggregate/Result/NumericResult.html +share/doc/mcollective/MCollective/Application.html +share/doc/mcollective/MCollective/Applications.html +share/doc/mcollective/MCollective/Cache.html +share/doc/mcollective/MCollective/Client.html +share/doc/mcollective/MCollective/Config.html +share/doc/mcollective/MCollective/Connector/ +share/doc/mcollective/MCollective/Connector.html +share/doc/mcollective/MCollective/Connector/Base.html +share/doc/mcollective/MCollective/DDL/ +share/doc/mcollective/MCollective/DDL.html +share/doc/mcollective/MCollective/DDL/AgentDDL.html +share/doc/mcollective/MCollective/DDL/Base.html +share/doc/mcollective/MCollective/DDL/DataDDL.html +share/doc/mcollective/MCollective/DDL/DiscoveryDDL.html +share/doc/mcollective/MCollective/DDL/ValidatorDDL.html +share/doc/mcollective/MCollective/DDLValidationError.html +share/doc/mcollective/MCollective/Data/ +share/doc/mcollective/MCollective/Data.html +share/doc/mcollective/MCollective/Data/Base.html +share/doc/mcollective/MCollective/Data/Result.html +share/doc/mcollective/MCollective/Discovery.html +share/doc/mcollective/MCollective/Facts/ +share/doc/mcollective/MCollective/Facts.html +share/doc/mcollective/MCollective/Facts/Base.html +share/doc/mcollective/MCollective/Generators/ +share/doc/mcollective/MCollective/Generators.html +share/doc/mcollective/MCollective/Generators/AgentGenerator.html +share/doc/mcollective/MCollective/Generators/Base.html +share/doc/mcollective/MCollective/Generators/DataGenerator.html +share/doc/mcollective/MCollective/InvalidRPCData.html +share/doc/mcollective/MCollective/Log.html +share/doc/mcollective/MCollective/Logger/ +share/doc/mcollective/MCollective/Logger.html +share/doc/mcollective/MCollective/Logger/Base.html +share/doc/mcollective/MCollective/Logger/Console_logger.html +share/doc/mcollective/MCollective/Logger/File_logger.html +share/doc/mcollective/MCollective/Logger/Syslog_logger.html +share/doc/mcollective/MCollective/Matcher/ +share/doc/mcollective/MCollective/Matcher.html +share/doc/mcollective/MCollective/Matcher/Parser.html +share/doc/mcollective/MCollective/Matcher/Scanner.html +share/doc/mcollective/MCollective/Message.html +share/doc/mcollective/MCollective/MissingRPCData.html +share/doc/mcollective/MCollective/MsgDoesNotMatchRequestID.html +share/doc/mcollective/MCollective/MsgTTLExpired.html +share/doc/mcollective/MCollective/NotTargettedAtUs.html +share/doc/mcollective/MCollective/Optionparser.html +share/doc/mcollective/MCollective/PluginManager.html +share/doc/mcollective/MCollective/PluginPackager/ +share/doc/mcollective/MCollective/PluginPackager.html +share/doc/mcollective/MCollective/PluginPackager/AgentDefinition.html +share/doc/mcollective/MCollective/PluginPackager/StandardDefinition.html +share/doc/mcollective/MCollective/RPC/ +share/doc/mcollective/MCollective/RPC.html +share/doc/mcollective/MCollective/RPC/ActionRunner.html +share/doc/mcollective/MCollective/RPC/Agent.html +share/doc/mcollective/MCollective/RPC/Audit.html +share/doc/mcollective/MCollective/RPC/Client.html +share/doc/mcollective/MCollective/RPC/Helpers.html +share/doc/mcollective/MCollective/RPC/Progress.html +share/doc/mcollective/MCollective/RPC/Reply.html +share/doc/mcollective/MCollective/RPC/Request.html +share/doc/mcollective/MCollective/RPC/Result.html +share/doc/mcollective/MCollective/RPC/Stats.html +share/doc/mcollective/MCollective/RPCAborted.html +share/doc/mcollective/MCollective/RPCError.html +share/doc/mcollective/MCollective/Registration/ +share/doc/mcollective/MCollective/Registration.html +share/doc/mcollective/MCollective/Registration/Base.html +share/doc/mcollective/MCollective/Runner.html +share/doc/mcollective/MCollective/RunnerStats.html +share/doc/mcollective/MCollective/SSL.html +share/doc/mcollective/MCollective/Security/ +share/doc/mcollective/MCollective/Security.html +share/doc/mcollective/MCollective/Security/Base.html +share/doc/mcollective/MCollective/SecurityValidationFailed.html +share/doc/mcollective/MCollective/Shell.html +share/doc/mcollective/MCollective/UnixDaemon.html +share/doc/mcollective/MCollective/UnknownRPCAction.html +share/doc/mcollective/MCollective/UnknownRPCError.html +share/doc/mcollective/MCollective/Util.html +share/doc/mcollective/MCollective/Validator.html +share/doc/mcollective/MCollective/ValidatorError.html +share/doc/mcollective/MCollective/WindowsDaemon.html +share/doc/mcollective/Object.html +share/doc/mcollective/README.html +share/doc/mcollective/Rakefile.html +share/doc/mcollective/String.html +share/doc/mcollective/Symbol.html +share/doc/mcollective/bin/ +share/doc/mcollective/bin/mc-call-agent.html +share/doc/mcollective/bin/mco.html +share/doc/mcollective/bin/mcollectived.html +share/doc/mcollective/created.rid +share/doc/mcollective/etc/ +share/doc/mcollective/etc/ssl/ +share/doc/mcollective/etc/ssl/PLACEHOLDER.html +share/doc/mcollective/etc/ssl/clients/ +share/doc/mcollective/etc/ssl/clients/PLACEHOLDER.html +share/doc/mcollective/images/ +share/doc/mcollective/images/brick.png +share/doc/mcollective/images/brick_link.png +share/doc/mcollective/images/bug.png +share/doc/mcollective/images/bullet_black.png +share/doc/mcollective/images/bullet_toggle_minus.png +share/doc/mcollective/images/bullet_toggle_plus.png +share/doc/mcollective/images/date.png +share/doc/mcollective/images/find.png +share/doc/mcollective/images/loadingAnimation.gif +share/doc/mcollective/images/macFFBgHack.png +share/doc/mcollective/images/package.png +share/doc/mcollective/images/page_green.png +share/doc/mcollective/images/page_white_text.png +share/doc/mcollective/images/page_white_width.png +share/doc/mcollective/images/plugin.png +share/doc/mcollective/images/ruby.png +share/doc/mcollective/images/tag_green.png +share/doc/mcollective/images/wrench.png +share/doc/mcollective/images/wrench_orange.png +share/doc/mcollective/images/zoom.png +share/doc/mcollective/index.html +share/doc/mcollective/js/ +share/doc/mcollective/js/darkfish.js +share/doc/mcollective/js/jquery.js +share/doc/mcollective/js/quicksearch.js +share/doc/mcollective/js/thickbox-compressed.js +share/doc/mcollective/lib/ +share/doc/mcollective/lib/mcollective/ +share/doc/mcollective/lib/mcollective/agent_rb.html +share/doc/mcollective/lib/mcollective/agents_rb.html +share/doc/mcollective/lib/mcollective/aggregate/ +share/doc/mcollective/lib/mcollective/aggregate/base_rb.html +share/doc/mcollective/lib/mcollective/aggregate/result/ +share/doc/mcollective/lib/mcollective/aggregate/result/base_rb.html +share/doc/mcollective/lib/mcollective/aggregate/result/collection_result_rb.html +share/doc/mcollective/lib/mcollective/aggregate/result/numeric_result_rb.html +share/doc/mcollective/lib/mcollective/aggregate/result_rb.html +share/doc/mcollective/lib/mcollective/aggregate_rb.html +share/doc/mcollective/lib/mcollective/application_rb.html +share/doc/mcollective/lib/mcollective/applications_rb.html +share/doc/mcollective/lib/mcollective/cache_rb.html +share/doc/mcollective/lib/mcollective/client_rb.html +share/doc/mcollective/lib/mcollective/config_rb.html +share/doc/mcollective/lib/mcollective/connector/ +share/doc/mcollective/lib/mcollective/connector/base_rb.html +share/doc/mcollective/lib/mcollective/connector_rb.html +share/doc/mcollective/lib/mcollective/data/ +share/doc/mcollective/lib/mcollective/data/base_rb.html +share/doc/mcollective/lib/mcollective/data/result_rb.html +share/doc/mcollective/lib/mcollective/data_rb.html +share/doc/mcollective/lib/mcollective/ddl/ +share/doc/mcollective/lib/mcollective/ddl/agentddl_rb.html +share/doc/mcollective/lib/mcollective/ddl/base_rb.html +share/doc/mcollective/lib/mcollective/ddl/dataddl_rb.html +share/doc/mcollective/lib/mcollective/ddl/discoveryddl_rb.html +share/doc/mcollective/lib/mcollective/ddl/validatorddl_rb.html +share/doc/mcollective/lib/mcollective/ddl_rb.html +share/doc/mcollective/lib/mcollective/discovery_rb.html +share/doc/mcollective/lib/mcollective/facts/ +share/doc/mcollective/lib/mcollective/facts/base_rb.html +share/doc/mcollective/lib/mcollective/facts_rb.html +share/doc/mcollective/lib/mcollective/generators/ +share/doc/mcollective/lib/mcollective/generators/agent_generator_rb.html +share/doc/mcollective/lib/mcollective/generators/base_rb.html +share/doc/mcollective/lib/mcollective/generators/data_generator_rb.html +share/doc/mcollective/lib/mcollective/generators_rb.html +share/doc/mcollective/lib/mcollective/log_rb.html +share/doc/mcollective/lib/mcollective/logger/ +share/doc/mcollective/lib/mcollective/logger/base_rb.html +share/doc/mcollective/lib/mcollective/logger/console_logger_rb.html +share/doc/mcollective/lib/mcollective/logger/file_logger_rb.html +share/doc/mcollective/lib/mcollective/logger/syslog_logger_rb.html +share/doc/mcollective/lib/mcollective/logger_rb.html +share/doc/mcollective/lib/mcollective/matcher/ +share/doc/mcollective/lib/mcollective/matcher/parser_rb.html +share/doc/mcollective/lib/mcollective/matcher/scanner_rb.html +share/doc/mcollective/lib/mcollective/matcher_rb.html +share/doc/mcollective/lib/mcollective/message_rb.html +share/doc/mcollective/lib/mcollective/monkey_patches_rb.html +share/doc/mcollective/lib/mcollective/optionparser_rb.html +share/doc/mcollective/lib/mcollective/pluginmanager_rb.html +share/doc/mcollective/lib/mcollective/pluginpackager/ +share/doc/mcollective/lib/mcollective/pluginpackager/agent_definition_rb.html +share/doc/mcollective/lib/mcollective/pluginpackager/standard_definition_rb.html +share/doc/mcollective/lib/mcollective/pluginpackager_rb.html +share/doc/mcollective/lib/mcollective/registration/ +share/doc/mcollective/lib/mcollective/registration/base_rb.html +share/doc/mcollective/lib/mcollective/registration_rb.html +share/doc/mcollective/lib/mcollective/rpc/ +share/doc/mcollective/lib/mcollective/rpc/actionrunner_rb.html +share/doc/mcollective/lib/mcollective/rpc/agent_rb.html +share/doc/mcollective/lib/mcollective/rpc/audit_rb.html +share/doc/mcollective/lib/mcollective/rpc/client_rb.html +share/doc/mcollective/lib/mcollective/rpc/helpers_rb.html +share/doc/mcollective/lib/mcollective/rpc/progress_rb.html +share/doc/mcollective/lib/mcollective/rpc/reply_rb.html +share/doc/mcollective/lib/mcollective/rpc/request_rb.html +share/doc/mcollective/lib/mcollective/rpc/result_rb.html +share/doc/mcollective/lib/mcollective/rpc/stats_rb.html +share/doc/mcollective/lib/mcollective/rpc_rb.html +share/doc/mcollective/lib/mcollective/runner_rb.html +share/doc/mcollective/lib/mcollective/runnerstats_rb.html +share/doc/mcollective/lib/mcollective/security/ +share/doc/mcollective/lib/mcollective/security/base_rb.html +share/doc/mcollective/lib/mcollective/security_rb.html +share/doc/mcollective/lib/mcollective/shell_rb.html +share/doc/mcollective/lib/mcollective/ssl_rb.html +share/doc/mcollective/lib/mcollective/unix_daemon_rb.html +share/doc/mcollective/lib/mcollective/util_rb.html +share/doc/mcollective/lib/mcollective/validator_rb.html +share/doc/mcollective/lib/mcollective/windows_daemon_rb.html +share/doc/mcollective/lib/mcollective_rb.html +share/doc/mcollective/rdoc.css +share/examples/mcollective/ +@sample ${SYSCONFDIR}/mcollective/ +share/examples/mcollective/client.cfg.dist +@sample ${SYSCONFDIR}/mcollective/client.cfg +share/examples/mcollective/data-help.erb +share/examples/mcollective/discovery-help.erb +share/examples/mcollective/facts.yaml.dist +@sample ${SYSCONFDIR}/mcollective/facts.yaml +share/examples/mcollective/metadata-help.erb +share/examples/mcollective/rpc-help.erb +share/examples/mcollective/server.cfg.dist +@sample ${SYSCONFDIR}/mcollective/server.cfg +share/examples/mcollective/ssl/ +@sample ${SYSCONFDIR}/mcollective/ssl/ +share/examples/mcollective/ssl/clients/ +@sample ${SYSCONFDIR}/mcollective/ssl/clients/ +share/mcollective/ +share/mcollective/plugins/ +share/mcollective/plugins/mcollective/ +share/mcollective/plugins/mcollective/agent/ +share/mcollective/plugins/mcollective/agent/discovery.rb +share/mcollective/plugins/mcollective/agent/rpcutil.ddl +share/mcollective/plugins/mcollective/agent/rpcutil.rb +share/mcollective/plugins/mcollective/aggregate/ +share/mcollective/plugins/mcollective/aggregate/average.rb +share/mcollective/plugins/mcollective/aggregate/sum.rb +share/mcollective/plugins/mcollective/aggregate/summary.rb +share/mcollective/plugins/mcollective/application/ +share/mcollective/plugins/mcollective/application/completion.rb +share/mcollective/plugins/mcollective/application/facts.rb +share/mcollective/plugins/mcollective/application/find.rb +share/mcollective/plugins/mcollective/application/help.rb +share/mcollective/plugins/mcollective/application/inventory.rb +share/mcollective/plugins/mcollective/application/ping.rb +share/mcollective/plugins/mcollective/application/plugin.rb +share/mcollective/plugins/mcollective/application/rpc.rb +share/mcollective/plugins/mcollective/audit/ +share/mcollective/plugins/mcollective/audit/logfile.rb +share/mcollective/plugins/mcollective/connector/ +share/mcollective/plugins/mcollective/connector/activemq.rb +share/mcollective/plugins/mcollective/connector/rabbitmq.rb +share/mcollective/plugins/mcollective/connector/stomp.rb +share/mcollective/plugins/mcollective/data/ +share/mcollective/plugins/mcollective/data/agent_data.ddl +share/mcollective/plugins/mcollective/data/agent_data.rb +share/mcollective/plugins/mcollective/data/fstat_data.ddl +share/mcollective/plugins/mcollective/data/fstat_data.rb +share/mcollective/plugins/mcollective/discovery/ +share/mcollective/plugins/mcollective/discovery/flatfile.ddl +share/mcollective/plugins/mcollective/discovery/flatfile.rb +share/mcollective/plugins/mcollective/discovery/mc.ddl +share/mcollective/plugins/mcollective/discovery/mc.rb +share/mcollective/plugins/mcollective/facts/ +share/mcollective/plugins/mcollective/facts/yaml_facts.rb +share/mcollective/plugins/mcollective/pluginpackager/ +share/mcollective/plugins/mcollective/pluginpackager/debpackage_packager.rb +share/mcollective/plugins/mcollective/pluginpackager/ospackage_packager.rb +share/mcollective/plugins/mcollective/pluginpackager/rpmpackage_packager.rb +share/mcollective/plugins/mcollective/pluginpackager/templates/ +share/mcollective/plugins/mcollective/pluginpackager/templates/debian/ +share/mcollective/plugins/mcollective/pluginpackager/templates/debian/Makefile.erb +share/mcollective/plugins/mcollective/pluginpackager/templates/debian/changelog.erb +share/mcollective/plugins/mcollective/pluginpackager/templates/debian/compat.erb +share/mcollective/plugins/mcollective/pluginpackager/templates/debian/control.erb +share/mcollective/plugins/mcollective/pluginpackager/templates/debian/copyright.erb +share/mcollective/plugins/mcollective/pluginpackager/templates/debian/rules.erb +share/mcollective/plugins/mcollective/pluginpackager/templates/redhat/ +share/mcollective/plugins/mcollective/pluginpackager/templates/redhat/rpm_spec.erb +share/mcollective/plugins/mcollective/registration/ +share/mcollective/plugins/mcollective/registration/agentlist.rb +share/mcollective/plugins/mcollective/security/ +share/mcollective/plugins/mcollective/security/aes_security.rb +share/mcollective/plugins/mcollective/security/psk.rb +share/mcollective/plugins/mcollective/security/ssl.rb +share/mcollective/plugins/mcollective/validator/ +share/mcollective/plugins/mcollective/validator/array_validator.ddl +share/mcollective/plugins/mcollective/validator/array_validator.rb +share/mcollective/plugins/mcollective/validator/ipv4address_validator.ddl +share/mcollective/plugins/mcollective/validator/ipv4address_validator.rb +share/mcollective/plugins/mcollective/validator/ipv6address_validator.ddl +share/mcollective/plugins/mcollective/validator/ipv6address_validator.rb +share/mcollective/plugins/mcollective/validator/length_validator.ddl +share/mcollective/plugins/mcollective/validator/length_validator.rb +share/mcollective/plugins/mcollective/validator/regex_validator.ddl +share/mcollective/plugins/mcollective/validator/regex_validator.rb +share/mcollective/plugins/mcollective/validator/shellsafe_validator.ddl +share/mcollective/plugins/mcollective/validator/shellsafe_validator.rb +share/mcollective/plugins/mcollective/validator/typecheck_validator.ddl +share/mcollective/plugins/mcollective/validator/typecheck_validator.rb +@rcscript ${RCDIR}/mcollectived diff --git a/ext/openbsd/port-files/mcollective/pkg/mcollectived.rc b/ext/openbsd/port-files/mcollective/pkg/mcollectived.rc new file mode 100644 index 0000000..7b6de9d --- /dev/null +++ b/ext/openbsd/port-files/mcollective/pkg/mcollectived.rc @@ -0,0 +1,10 @@ +#!/bin/sh + +daemon="${TRUEPREFIX}/sbin/mcollectived" + +. /etc/rc.d/rc.subr + +pexp=".*ruby.* ${daemon}${daemon_flags:+ ${daemon_flags}}" +rc_reload=NO + +rc_cmd $1 diff --git a/ext/osx/README b/ext/osx/README new file mode 100644 index 0000000..72750b5 --- /dev/null +++ b/ext/osx/README @@ -0,0 +1,15 @@ +This script will automatically build Mac packages based on your source +tarball. The only argument is the path to the untarred source directory. + +It automatically determines the MCollective version so it will only work +with 0.4.9 and forward. It loads mcollective in ruby so it requires +that the stomp rubygem be installed. + +I could just grep the mcollective.rb script but I wanted it to be future proof. +It also requires XCode be installed to have PackageMaker available. It sets +the server.cfg file daemonize setting to 0 since my launchd plist will handle +that. I also have launchd set to restart mcollectived if it stops running. + +Let me know if you run in to trouble with it. + +Carl Caum - carl _at_ carlcaum.com diff --git a/ext/osx/bldmacpkg b/ext/osx/bldmacpkg new file mode 100644 index 0000000..6f3dc5f --- /dev/null +++ b/ext/osx/bldmacpkg @@ -0,0 +1,133 @@ +#!/bin/bash + +MPATH='' +BETCDIR='/etc/mcollective' +BRUBYDIR='/Library/Ruby/Site/1.8' +BSBINDIR='/usr/sbin' +BBINDIR='/usr/bin' +BLIBEXECDIR='/usr/libexec/mcollective' +BDOCDIR='/usr/share/doc/mcollective' +BLAUNCHDIR='/Library/LaunchDaemons' + +if [ -z $1 ]; then + echo 'Please give the path to the MCollective source directory' + exit 1 +else + MPATH=$1 +fi + +function msg_stomp { + echo 'It is recommended to install stomp on this system using ruby gems' + exit 2 +} + +function msg_xcode { + echo 'It is required to have the latest XCode installed' + exit 3 +} + +#Make sure we have stomp so we can load mcollective +/usr/bin/ruby < $tmpdir/$BLAUNCHDIR/org.marionette-collective.mcollective.plist < + + + + EnvironmentVariables + + PATH + /sbin:/usr/sbin:/bin:/usr/bin + RUBYLIB + /Library/Ruby/Site/1.8 + + Label + org.marionette-collective.mcollective + OnDemand + + KeepAlive + + ProgramArguments + + /usr/sbin/mcollectived + --config=/etc/mcollective/server.cfg + + RunAtLoad + + ServiceDescription + MCollective Server + ServiceIPC + + + +EOF + +#launchd complains if the permissions aren't right +chmod 644 $tmpdir/$BLAUNCHDIR/org.marionette-collective.mcollective.plist + +#Make our Packages. This requires XCode be installed +/Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker -r $tmpdir --version $mcversion --title "MCollective" -l / -o MCollective_$mcversion.pkg -i org.marionette-collective.mcollective +/Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker -r $common_tmpdir --version $mcversion --title "MCollective Common" -l / -o MCollective-Common_$mcversion.pkg -i org.marionette-collective.mcollective-common +/Developer/Applications/Utilities/PackageMaker.app/Contents/MacOS/PackageMaker -r $client_tmpdir --version $mcversion --title "MCollective Client" -l / -o MCollective-Client_$mcversion.pkg -i org.marionette-collective.mcollective-client + +#Clean up +rm -rf $tmpdir +rm -rf $common_tmpdir +rm -rf $client_tmpdir diff --git a/ext/perl/mc-find-hosts.pl b/ext/perl/mc-find-hosts.pl new file mode 100644 index 0000000..15f5f4f --- /dev/null +++ b/ext/perl/mc-find-hosts.pl @@ -0,0 +1,80 @@ +#!/usr/bin/perl + +# A simple Perl client for mcollective that just demonstrates +# how to construct requests, send them and process results. +# +# This is in effect a mc-find-hosts equivelant, you can fill in +# filters in the request and only the matching ones will reply. +# +# For this to work you need the SSL security plugin in MCollective +# 1.0.0 set to operate in YAML mode. + +use YAML::Syck; +use Digest::MD5 qw(md5 md5_hex md5_base64); +use Crypt::OpenSSL::RSA; +use MIME::Base64; +use Net::STOMP::Client; +use Data::Dumper; + +# The topics from your activemq, /topic/mcollective_dev/... +$mcollective_prefix = "mcollective_dev"; + +# Path to your SSL private key and what it's called so the +# mcollectived will load yours +$ssl_private_key = "/path/to/your.pem"; +$ssl_private_key_name = "you"; + +# A string representing your sending host +$mcollective_client_identity = "devel.your.com-perl"; + +# Stomp connection parameters +$stomp_host = "localhost"; +$stomp_port = 6163; +$stomp_user = "your"; +$stomp_password = "secret"; + +$YAML::Syck::ImplicitTyping = 1; + +$request{":msgtime"} = time(); +$request{":filter"}{"identity"} = []; +$request{":filter"}{"fact"} = []; +$request{":filter"}{"agent"} = []; +$request{":filter"}{"cf_class"} = []; +$request{":requestid"} = md5_hex(time() . $$); +$request{":callerid"} = "cert=${ssl_private_key_name}"; +$request{":senderid"} = $mcollective_client_identity; +$request{":body"} = Dump("ping"); +$request{":msgtarget"} = "/topic/${mcollective_prefix}.discovery.command"; + +$key = ""; + +open(SSL, $ssl_private_key); + while() { + $key = $key . $_; + } +close(SSL); + +$rsa = Crypt::OpenSSL::RSA->new_private_key($key); +$request{":hash"} = encode_base64($rsa->sign($request{":body"})); + +$mcrequest = Dump(\%request); + +$stomp = Net::STOMP::Client->new(host => $stomp_host, port => $stomp_port); +$stomp->connect(login => $stomp_user, passcode => $stomp_password); + +$stomp->message_callback(sub { + my ($self, $frame) = @_; + + $mc_reply = Load($frame->body); + $mc_body = Load($mc_reply->{":body"}); + print $mc_reply->{":senderid"} . "> " . $mc_body . "\n"; + + return($self); + }); + +$stomp->subscribe(destination => "/topic/${mcollective_prefix}.discovery.reply"); +$stomp->send(destination => "/topic/${mcollective_prefix}.discovery.command", body => $mcrequest); +$stomp->wait_for_frames(callback => sub { return(0) }, timeout => 5); +$stomp->disconnect(); + + diff --git a/ext/redhat/mcollective.init b/ext/redhat/mcollective.init new file mode 100755 index 0000000..90c9b59 --- /dev/null +++ b/ext/redhat/mcollective.init @@ -0,0 +1,139 @@ +#!/bin/sh +# +# mcollective Application Server for STOMP based agents +# +# chkconfig: - 24 76 +# +# description: mcollective lets you build powerful Stomp compatible middleware clients in ruby without having to worry too +# much about all the setup and management of a Stomp connection, it also provides stats, logging and so forth +# as a bonus. +# +### BEGIN INIT INFO +# Provides: mcollective +# Required-Start: $remote_fs +# Required-Stop: $remote_fs +# Short-Description: Start daemon at boot time +# Description: Enable service provided by daemon. +### END INIT INFO + +mcollectived="/usr/sbin/mcollectived" +pidfile="/var/run/mcollectived.pid" +if [ -d /var/lock/subsys ]; then + # RedHat/CentOS/etc who use subsys + lockfile="/var/lock/subsys/mcollective" +else + # The rest of them + lockfile="/var/lock/mcollective" +fi + +# Check that binary exists +if ! [ -f $mcollectived ]; then + echo "mcollectived binary not found" + exit 5 +fi + +# Source function library. +. /etc/init.d/functions + +if [ -f /etc/sysconfig/mcollective ]; then + . /etc/sysconfig/mcollective +fi + +# Determine if we can use the -p option to daemon, killproc, and status. +# RHEL < 5 can't. +if status | grep -q -- '-p' 2>/dev/null; then + daemonopts="--pidfile $pidfile" + pidopts="-p $pidfile" +fi + +start() { + echo -n "Starting mcollective: " + # Only try to start if not already started + if ! rh_status_q; then + daemon ${daemonopts} ${mcollectived} --pid=${pidfile} --config="/etc/mcollective/server.cfg" + fi + # This will be 0 if mcollective is already running + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && touch ${lockfile} + return $RETVAL +} + +stop() { + echo -n "Shutting down mcollective: " + # If running, try to stop it + if rh_status_q; then + killproc ${pidopts} -d 10 ${mcollectived} + else + # Non-zero status either means lockfile and pidfile need cleanup (1 and 2) + # or the process is already stopped (3), so we can just call true to + # trigger the cleanup that happens below. + true + fi + RETVAL=$? + echo + [ $RETVAL = 0 ] && rm -f ${lockfile} ${pidfile} + return $RETVAL +} + +restart() { + stop + start +} + +reload_agents() { + echo -n "Reloading mcollective agents: " + killproc ${pidopts} ${mcollectived} -USR1 + RETVAL=$? + echo + return $RETVAL +} + +reload_loglevel() { + echo -n "Cycling mcollective logging level: " + killproc ${pidopts} ${mcollectived} -USR2 + RETVAL=$? + echo + return $RETVAL +} + +rh_status() { + status ${pidopts} ${mcollectived} + RETVAL=$? + return $RETVAL +} + +rh_status_q() { + rh_status >/dev/null 2>&1 +} + +# See how we were called. +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + condrestart) + rh_status_q || exit 0 + restart + ;; + reload-agents) + reload_agents + ;; + reload-loglevel) + reload_loglevel + ;; + status) + rh_status + ;; + *) + echo "Usage: mcollectived {start|stop|restart|condrestart|reload-agents|reload-loglevel|status}" + RETVAL=2 + ;; +esac +exit $RETVAL diff --git a/ext/redhat/mcollective.spec b/ext/redhat/mcollective.spec new file mode 100644 index 0000000..d11def7 --- /dev/null +++ b/ext/redhat/mcollective.spec @@ -0,0 +1,130 @@ +%{!?ruby_sitelib: %global ruby_sitelib %(ruby -rrbconfig -e "puts RbConfig::CONFIG['sitelibdir']")} +%define release %{rpm_release}%{?dist} + +Summary: Application Server for hosting Ruby code on any capable middleware +Name: mcollective +Version: %{version} +Release: %{release} +Group: System Environment/Daemons +License: ASL 2.0 +URL: http://puppetlabs.com/mcollective/introduction/ +Source0: http://downloads.puppetlabs.com/mcollective/%{name}-%{version}.tgz +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) +BuildRequires: ruby +BuildRequires: ruby(abi) >= 1.8 +Requires: mcollective-common = %{version}-%{release} +Packager: R.I.Pienaar +BuildArch: noarch + +%package common +Summary: Common libraries for the mcollective clients and servers +Group: System Environment/Libraries +Requires: ruby +Requires: ruby(abi) >= 1.8 +Requires: rubygems +Requires: rubygem(stomp) + +%description common +The Marionette Collective: + +Common libraries for the mcollective clients and servers + +%package client +Summary: Client tools for the mcollective Application Server +Requires: mcollective-common = %{version}-%{release} +Group: Applications/System + +%description client +The Marionette Collective: + +Client tools for the mcollective Application Server + +%description +The Marionette Collective: + +Server for the mcollective Application Server + +%prep +%setup -q + +%build + +%install +rm -rf %{buildroot} +%{__install} -d -m0755 %{buildroot}/%{ruby_sitelib}/mcollective +%{__install} -d -m0755 %{buildroot}%{_bindir} +%{__install} -d -m0755 %{buildroot}%{_sbindir} +%{__install} -d -m0755 %{buildroot}%{_sysconfdir}/init.d +%{__install} -d -m0755 %{buildroot}%{_libexecdir}/mcollective/ +%{__install} -d -m0755 %{buildroot}%{_sysconfdir}/mcollective +%{__install} -d -m0755 %{buildroot}%{_sysconfdir}/mcollective/plugin.d +%{__install} -d -m0755 %{buildroot}%{_sysconfdir}/mcollective/ssl +%{__install} -d -m0755 %{buildroot}%{_sysconfdir}/mcollective/ssl/clients +%{__install} -m0755 bin/mcollectived %{buildroot}%{_sbindir}/mcollectived +%{__install} -m0640 etc/server.cfg.dist %{buildroot}%{_sysconfdir}/mcollective/server.cfg +%{__install} -m0644 etc/client.cfg.dist %{buildroot}%{_sysconfdir}/mcollective/client.cfg +%{__install} -m0444 etc/facts.yaml.dist %{buildroot}%{_sysconfdir}/mcollective/facts.yaml +%{__install} -m0444 etc/rpc-help.erb %{buildroot}%{_sysconfdir}/mcollective/rpc-help.erb +%{__install} -m0444 etc/data-help.erb %{buildroot}%{_sysconfdir}/mcollective/data-help.erb +%{__install} -m0444 etc/discovery-help.erb %{buildroot}%{_sysconfdir}/mcollective/discovery-help.erb +%{__install} -m0444 etc/metadata-help.erb %{buildroot}%{_sysconfdir}/mcollective/metadata-help.erb +%{__install} -m0444 etc/msg-help.erb %{buildroot}%{_sysconfdir}/mcollective/msg-help.erb +%if 0%{?suse_version} +%{__install} -m0755 mcollective.init %{buildroot}%{_sysconfdir}/init.d/mcollective +%else +%{__install} -m0755 ext/redhat/mcollective.init %{buildroot}%{_sysconfdir}/init.d/mcollective +%endif + + +cp -R lib/* %{buildroot}/%{ruby_sitelib}/ +cp -R plugins/* %{buildroot}%{_libexecdir}/mcollective/ +cp bin/mc-* %{buildroot}%{_sbindir}/ +cp bin/mco %{buildroot}%{_bindir}/ +chmod 0755 %{buildroot}%{_sbindir}/* + +%clean +rm -rf %{buildroot} + +%post +/sbin/chkconfig --add mcollective || : + +%postun +if [ "$1" -ge 1 ]; then + /sbin/service mcollective condrestart &>/dev/null || : +fi + +%preun +if [ "$1" = 0 ] ; then + /sbin/service mcollective stop > /dev/null 2>&1 + /sbin/chkconfig --del mcollective || : +fi + +%files common +%doc COPYING +%{ruby_sitelib}/mcollective.rb +%{ruby_sitelib}/mcollective +%{_libexecdir}/mcollective/mcollective +%dir %{_sysconfdir}/mcollective +%dir %{_sysconfdir}/mcollective/ssl +%config %{_sysconfdir}/mcollective/*.erb + +%files client +%attr(0755, root, root)%{_sbindir}/mc-call-agent +%attr(0755, root, root)%{_bindir}/mco +%doc COPYING +%config(noreplace)%{_sysconfdir}/mcollective/client.cfg +%{_libexecdir}/mcollective/mcollective/application +%{_libexecdir}/mcollective/mcollective/pluginpackager + +%files +%doc COPYING +%{_sbindir}/mcollectived +%{_sysconfdir}/init.d/mcollective +%config(noreplace)%{_sysconfdir}/mcollective/server.cfg +%config(noreplace)%{_sysconfdir}/mcollective/facts.yaml +%dir %{_sysconfdir}/mcollective/ssl/clients +%config(noreplace)%{_sysconfdir}/mcollective/plugin.d + +%changelog +* Tue Nov 03 2009 R.I.Pienaar +- First release diff --git a/ext/solaris/README b/ext/solaris/README new file mode 100644 index 0000000..4bcbf20 --- /dev/null +++ b/ext/solaris/README @@ -0,0 +1,33 @@ +Building +-------- + +Requirements, you can get them from opencsw: +- coreuitls (CSWcoreutils) +- gmake (CSWgmake) +- ggrep (CSWggrep) + +Just run ./build on your solaris system. + +Running +------- + +Requirements, get them from opencsw: +- ruby (CSWruby) +- rubygems (CSWrubygems) + +Run requirements +- rubystomp library + http://stomp.codehaus.org/Ruby+Client + Up and till version 1.0.4 it is a single file. Put in /opt/csw/lib/ruby/site_ruby/1.8/ + +Configuration +------------- + +/etc/mcollective/server.cfg + +Put the plugins in: +libdir = /opt/csw/share/mcollective/plugins + +Credits +------- +Rudy Gevaert diff --git a/ext/solaris/build b/ext/solaris/build new file mode 100755 index 0000000..06d7190 --- /dev/null +++ b/ext/solaris/build @@ -0,0 +1,68 @@ +#!/opt/csw/bin/gmake -f -d +# -*- makefile -*- + +BUILDDIR = solaris/tmp +PKG = solaris/pkg +DESTDIR = ${CURDIR}/${BUILDDIR} +PKGDIR = ${CURDIR}/${PKG} +PKGNAME = CSWmcollective +VERSION = $(shell cd ../.. ; RUBYLIB=./lib /opt/csw/bin/ruby18 -r mcollective -e 'puts MCollective::VERSION' ) +# If we checked out from git: +ifeq ($(VERSION),@DEVELOPMENT_VERSION@) + VERSION = $(shell ggrep "PROJ_VERSION = " ../../Rakefile | cut -d' ' -f3 | sed -e 's/"//g') +endif +RELEASE = 1 +PKGVERSION = ${VERSION}-${RELEASE}\,REV=$(shell date +%Y.%m.%d) +RUBY_VERSION = 1.8 +RUBY_SITE = ${DESTDIR}/opt/csw/lib/ruby/site_ruby/${RUBY_VERSION} + +install: + # install directories + ginstall -d $(DESTDIR) + ginstall -g root -d $(DESTDIR)/opt + ginstall -g sys -d $(DESTDIR)/var $(DESTDIR)/var/lock $(DESTDIR)/etc $(DESTDIR)/etc/opt + ginstall -g bin -d $(DESTDIR)/var/opt $(DESTDIR)/var/opt/csw $(DESTDIR)/var/opt/csw/svc $(DESTDIR)/var/opt/csw/svc/manifest $(DESTDIR)/var/opt/csw/svc/manifest/network + ginstall -g bin -d $(DESTDIR)/opt/csw/lib $(DESTDIR)/opt/csw/lib/svc $(DESTDIR)/opt/csw/lib/svc/method + ginstall -g bin -d $(DESTDIR)/opt/csw $(DESTDIR)/opt/csw/lib $(DESTDIR)/opt/csw/sbin $(DESTDIR)/opt/csw/bin + ginstall -g bin -d $(DESTDIR)/opt/csw/lib/ruby $(DESTDIR)/opt/csw/lib/ruby/site_ruby $(DESTDIR)/opt/csw/lib/ruby/site_ruby/$(RUBY_VERSION) + ginstall -g bin -d $(DESTDIR)/etc/opt/csw $(DESTDIR)/etc/opt/csw/mcollective + ginstall -g bin -d $(DESTDIR)/opt/csw/share $(DESTDIR)/opt/csw/share/mcollective + + # install binaries + ginstall -g bin $(CURDIR)/../../bin/mc-* $(DESTDIR)/opt/csw/sbin/ + ginstall -g bin $(CURDIR)/../../bin/mco $(DESTDIR)/opt/csw/sbin/ + ginstall -g bin $(CURDIR)/../../bin/mcollectived $(DESTDIR)/opt/csw/sbin/mcollectived + # install libraries + gcp -a $(CURDIR)/../../lib/* $(RUBY_SITE)/ + chgrp -R bin $(RUBY_SITE)/ + # install example config files + gcp -a $(CURDIR)/../../etc/* $(DESTDIR)/etc/opt/csw/mcollective/ + grm $(DESTDIR)/etc/opt/csw/mcollective/ssl/PLACEHOLDER + grm $(DESTDIR)/etc/opt/csw/mcollective/ssl/clients/PLACEHOLDER + chgrp -R bin $(DESTDIR)/etc/opt/csw/mcollective/ + # install plugins + gcp -a $(CURDIR)/../../plugins $(DESTDIR)/opt/csw/share/mcollective/ + # install docs + #ginstall -d $(DESTDIR)/opt/csw/doc $(DESTDIR)/opt/csw/doc/mcollective/ + #gcp -a $(CURDIR)/../../doc/ $(DESTDIR)/opt/cs/doc/mcollective + + ginstall -g bin $(CURDIR)/mcollective.init $(DESTDIR)/opt/csw/lib/svc/method/svc-cswmcollectived + ginstall -g bin $(CURDIR)/cswmcollectived.xml $(DESTDIR)/var/opt/csw/svc/manifest/network + + (cat prototype.head; pkgproto $(DESTDIR)=/ ) > solaris/prototype + mkdir $(PKGDIR) || true + ginstall postinstall solaris/ + ginstall postremove solaris/ + ginstall preremove solaris/ + + ginstall pkginfo solaris/ + (echo PKG=${PKGNAME} ) >> solaris/pkginfo + (echo VERSION=${PKGVERSION} ) >> solaris/pkginfo + (cd solaris/ ; pkgmk -o -d $(PKGDIR)) + pkgtrans -s $(PKGDIR) $(CURDIR)/$(PKGNAME)-$(PKGVERSION)-`uname -s``uname -r`-all-CSW.pkg $(PKGNAME) + +clean: + grm -rf $(DESTDIR) + grm -rf $(PKGDIR) + grm -f solaris/prototype + grm -f $(PKGNAME)-$(SOLARIS_VERSION)-`uname -s``uname -r`-all-CSW.pkg diff --git a/ext/solaris/cswmcollectived.xml b/ext/solaris/cswmcollectived.xml new file mode 100644 index 0000000..8f0b5c8 --- /dev/null +++ b/ext/solaris/cswmcollectived.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ext/solaris/depend b/ext/solaris/depend new file mode 100644 index 0000000..6abc643 --- /dev/null +++ b/ext/solaris/depend @@ -0,0 +1,3 @@ +P CWSruby ruby +P CWSrubystomp rubystomp +P CSWrubygems rubygems diff --git a/ext/solaris/mcollective.init b/ext/solaris/mcollective.init new file mode 100755 index 0000000..e94df91 --- /dev/null +++ b/ext/solaris/mcollective.init @@ -0,0 +1,96 @@ +#!/bin/sh +# +# mcollective Application Server for STOMP based agents +# +# description: mcollective lets you build powerful Stomp compatible middleware clients in ruby without having to worry too +# much about all the setup and management of a Stomp connection, it also provides stats, logging and so forth +# as a bonus. +# + +RUBYLIB=/opt/csw/lib/ruby/site_ruby/1.8:$RUBYLIB +export RUBYLIB + +mcollectived="/opt/csw/sbin/mcollectived" + +lock="/var/lock/mcollective" + +# PID directory +pidfile="/var/run/mcollectived.pid" + +# Check that binary exists +if [ ! -f $mcollectived ] +then + echo "mcollectived binary not found" + exit 1 +fi + +# See how we were called. +case "$1" in + start) + if [ -f ${lock} ]; then + # we were not shut down correctly + if [ -s ${pidfile} ]; then + kill `cat ${pidfile}` >/dev/null 2>&1 + fi + rm -f ${pidfile} + + rm -f ${lock} + sleep 2 + fi + + rm -f ${pidfile} + + ${mcollectived} --pid=${pidfile} --config="/etc/mcollective/server.cfg" + if [ $? = 0 ]; then + touch $lock + exit 0 + else + exit 1 + fi + ;; + stop) + if [ -s ${pidfile} ]; then + kill `cat ${pidfile}` >/dev/null 2>&1 + fi + rm -f ${pidfile} + + rm -f $lock + ;; + restart) + $0 stop + sleep 2 + $0 start + ;; + condrestart) + if [ -f $lock ]; then + $0 stop + # avoid race + sleep 2 + $0 start + fi + ;; + status) + if [ -f ${lock} ]; then + if [ -s ${pidfile} ]; then + if [ -e /proc/`cat ${pidfile}` ]; then + echo "mcollectived (`cat ${pidfile}`) is running" + exit 0 + else + echo "mcollectived (`cat ${pidfile}`) is NOT running" + exit 1 + fi + fi + else + echo "mcollectived: service not started" + exit 1 + fi + ;; + force-reload) + echo "not implemented" + ;; + *) + echo "Usage: $0 {start|stop|restart|condrestart|status}" + exit 1 + ;; +esac +exit 0 diff --git a/ext/solaris/pkginfo b/ext/solaris/pkginfo new file mode 100644 index 0000000..bba40e8 --- /dev/null +++ b/ext/solaris/pkginfo @@ -0,0 +1,6 @@ +NAME=mcollective- build server orchestration or parallel job execution systems +CATEGORY=network +DESC=The Marionette Collective aka. mcollective is a framework to build server orchestration or parallel job execution systems. +ARCH=all +EMAIL=rudy.gevaert@ugent.be +VENDOR=Puppetlabs diff --git a/ext/solaris/postinstall b/ext/solaris/postinstall new file mode 100644 index 0000000..1f2d093 --- /dev/null +++ b/ext/solaris/postinstall @@ -0,0 +1,6 @@ +#! /bin/bash + +PKG_INSTALL_ROOT=${PKG_INSTALL_ROOT:-/} + +/usr/bin/test -d $PKG_INSTALL_ROOT/etc/mcollective || /usr/sbin/chroot $PKG_INSTALL_ROOT /usr/bin/ln -s /etc/opt/csw/mcollective /etc/mcollective +/usr/sbin/chroot $PKG_INSTALL_ROOT /usr/sbin/svccfg import /var/opt/csw/svc/manifest/network/cswmcollectived.xml || /bin/true diff --git a/ext/solaris/postremove b/ext/solaris/postremove new file mode 100644 index 0000000..4b65673 --- /dev/null +++ b/ext/solaris/postremove @@ -0,0 +1,3 @@ +/usr/sbin/svcadm disable svc:network/cswmcollectived 2>/dev/null || /bin/true +/usr/sbin/svccfg delete svc:network/cswmcollectived 2>/dev/null || /bin/true +rm /etc/mcollective || /bin/true diff --git a/ext/solaris/preremove b/ext/solaris/preremove new file mode 100644 index 0000000..170584d --- /dev/null +++ b/ext/solaris/preremove @@ -0,0 +1,2 @@ +/usr/sbin/svcadm disable svc:network/cswmcollectived 2>/dev/null || /bin/true +/usr/sbin/svccfg delete svc:network/cswmcollectived 2>/dev/null || /bin/true diff --git a/ext/solaris/prototype.head b/ext/solaris/prototype.head new file mode 100644 index 0000000..db90289 --- /dev/null +++ b/ext/solaris/prototype.head @@ -0,0 +1,4 @@ +i pkginfo +i postinstall +i preremove +i postremove diff --git a/ext/solaris/setversion b/ext/solaris/setversion new file mode 100755 index 0000000..8829f0e --- /dev/null +++ b/ext/solaris/setversion @@ -0,0 +1,9 @@ +#! /bin/bash + +IN=$1 +OUT=$2 + +SOLARIS_VERSION=`ggrep ^VERSION solaris/pkginfo | cut -d = -f2` + +sed 's/VERSION="none"/VERSION="'"$SOLARIS_VERSION"'"/' $IN > $OUT +chmod 755 $OUT diff --git a/ext/solaris11/Makefile b/ext/solaris11/Makefile new file mode 100644 index 0000000..a1e6f0e --- /dev/null +++ b/ext/solaris11/Makefile @@ -0,0 +1,46 @@ +#!/usr/bin/make -f + +DESTDIR= + +build: + +clean: + +install: install-bin install-lib install-conf install-plugins # install-doc + +install-bin: + install -d $(DESTDIR)/usr/sbin + install -d $(DESTDIR)/usr/bin + cp bin/mc-* $(DESTDIR)/usr/sbin + cp bin/mco $(DESTDIR)/usr/bin + cp bin/mcollectived $(DESTDIR)/usr/sbin/mcollectived + cp COPYING $(DESTDIR)/ + +install-lib: + install -d $(DESTDIR)/usr/ruby/1.8/lib/ruby/site_ruby/1.8/ + cp -rp lib/* $(DESTDIR)/usr/ruby/1.8/lib/ruby/site_ruby/1.8/ + +install-conf: + install -d $(DESTDIR)/etc/mcollective/ + install -d $(DESTDIR)/etc/init.d + cp -r etc/* $(DESTDIR)/etc/mcollective/ + cp mcollective.init $(DESTDIR)/etc/init.d/mcollective + rm $(DESTDIR)/etc/mcollective/ssl/PLACEHOLDER + rm $(DESTDIR)/etc/mcollective/ssl/clients/PLACEHOLDER + +install-plugins: + install -d $(DESTDIR)/usr/share/mcollective/ + cp -rp plugins $(DESTDIR)/usr/share/mcollective/ + +install-doc: + install -d $(DESTDIR)/usr/share/doc/ + cp -rp doc $(DESTDIR)/usr/share/doc/mcollective + +uninstall: + rm -f $(DESTDIR)/usr/sbin/mcollectived + rm -rf $(DESTDIR)/usr/ruby/1.8/lib/ruby/site_ruby/1.8/mcollective* + rm -rf $(DESTDIR)/usr/share/mcollective + rm -rf $(DESTDIR)/etc/mcollective + +.PHONY: build clean install uninstall + diff --git a/ext/solaris11/README.md b/ext/solaris11/README.md new file mode 100644 index 0000000..17d70e9 --- /dev/null +++ b/ext/solaris11/README.md @@ -0,0 +1,104 @@ +Requirements +------------ + +- Ruby (pkg install pkg:/runtime/ruby-18) +- Header files (pkg install system/header) +- GCC to install JSON (pkg install developer/gcc-3) +- Stomp: gem install stomp +- Ruby-JSON: gem install json + +Installation +------------ + +Clone the github repository and install as root: + + $ cd marionette-collective + $ make -f ext/solaris11/Makefile install + +This will use / as a default destination root directory. + +IPS package +----------- + +To create an IPS package, follow the excellent guide at: +http://www.neuhalfen.name/blog/2011/07/02/Solaris11-Packaging-IPS_simple_packages/ + +To create a basic IPS repository (and start the associated services): + + # zfs create rpool/IPS + # zfs set atime=off rpool/IPS + # zfs set mountpoint=/IPS rpool/IPS + # mkdir /IPS/Solaris11 + # svcadm enable application/pkg/server + # svccfg -s application/pkg/server setprop pkg/inst_root=/IPS/Solaris11 + # svccfg -s application/pkg/server setprop pkg/readonly=false + # pkgrepo create /IPS/Solaris11/ + # pkgrepo set -s /IPS/Solaris11 publisher/prefix=legrand.im + # pkgrepo -s /IPS/Solaris11 refresh + # svcadm refresh application/pkg/server + # svcadm enable application/pkg/server + # pkg set-publisher -O http://localhost:80 legrand.im + +To create and send the package itself, from the guide above: + + # mkdir ~/package + # cd /marionette-collective + # cat Makefile | sed 's/DESTDIR=$/DESTDIR=~\/package/' > Makefile.package + # make -f ext/solaris11/Makefile.package install + # pkg install pkg:/file/gnu-findutils + # export ROOT=/ + # export description="MCollective" + # export user="root" + # export group="root" + # cd ~/package + # cat > ../send.sh << "EOF" + #!/bin/sh + export PKGSEND="pkgsend -s http://localhost:80" + eval `$PKGSEND open mcollective@1.1-1` + $PKGSEND add license ./COPYING license=lorem_ipsum + $PKGSEND add set name=description value="${description}" + EOF + # gfind . -type d -not -name . -printf "\$PKGSEND add dir mode=%m owner=${user} group=${group} path=$ROOT/%h/%f \n" >> ../send.sh + # gfind . -type f -not -name LICENSE -printf "\$PKGSEND add file %h/%f mode=%m owner=${user} group=${group} path=$ROOT/%h/%f \n" >> ../send.sh + # gfind . -type l -not -name LICENSE -printf "\$PKGSEND add link path=%h/%f target=%l \n" >> ../send.sh + # echo '$PKGSEND close' >> ../send.sh + # sh -x ../send.sh + +The package can then be installed with: + + # pkg install pkg://legrand.im/mcollective + +Configuration +------------- + +There is no packaged configuration; you can use the following example: + + # cat > /etc/mcollective/client.cfg << "EOF" + topicprefix = /topic/ + main_collective = mcollective + collectives = mcollective + libdir = /usr/share/mcollective/plugins + logfile = /dev/null + loglevel = info + # Plugins + securityprovider = psk + plugin.psk = unset + connector = stomp + plugin.stomp.host = mqserver + plugin.stomp.port = 6163 + plugin.stomp.user = mcollective + plugin.stomp.password = changeme + # Facts + factsource = yaml + plugin.yaml = /etc/mcollective/facts.yaml + EOF + +License +------ + +http://creativecommons.org/publicdomain/zero/1.0/ + +To the extent possible under law, Mathieu Legrand has waived all copyright and related or +neighboring rights to this work. This work is published from: Singapore. + + diff --git a/ext/stompclient b/ext/stompclient new file mode 100755 index 0000000..1b1b3b5 --- /dev/null +++ b/ext/stompclient @@ -0,0 +1,156 @@ +#!/usr/bin/env ruby +# == Synopsis +# +# stompclient: Generic client to consume and produce STOMP queues and topics, tested against +# Apache Active MQ +# +# == Description +# A simple client that can connect to an STOMP server, subscribe to topics and queues and also +# send to topics and queues. +# +# == Usage +# stompclient [OPTIONS] +# +# --help, -h: +# Show Help +# +# --server, -s +# The server to connect to, can also be set in STOMP_SERVER environment variable +# +# --port, -p +# The port to connect to, default to 6163 +# +# --user, -u +# The user to connect as, can also be set in STOMP_USER environment variable +# +# --password, -P +# The password to use, can also be set in STOMP_PASSWORD environment variable +# +# When connected to a server, use the 'help' command to see further information about +# using the client, common commands that can be issued are: +# +# - subscribe /topic/foo: Subscribes to topic 'foo' +# - /topic/foo bar: Sends 'bar' to the topic 'foo' +# - details: Toggle the display or timestamp and topic or queue information for each message +# +# +# == Changelog +# - 20 December 2009 Include into MCollective +# - 17 March 2009 Initial release +# +# R.I.Pienaar more information at www.devco.net +# +# Licensed under the Apache License, Version 2.0 + +require 'rubygems' +require 'stomp' +require 'readline' +require 'thread' +require 'getoptlong' + +opts = GetoptLong.new( + [ '--server', '-s', GetoptLong::REQUIRED_ARGUMENT], + [ '--port', '-p', GetoptLong::REQUIRED_ARGUMENT], + [ '--user', '-u', GetoptLong::REQUIRED_ARGUMENT], + [ '--password', '-P', GetoptLong::REQUIRED_ARGUMENT], + [ '--help', '-h', GetoptLong::NO_ARGUMENT] +) + +@user = ENV["STOMP_USER"]; +@password = ENV["STOMP_PASSWORD"] +@server = ENV["STOMP_SERVER"] +@port = ENV["STOMP_PORT"] || 6163 + +opts.each { |opt, arg| + case opt + when '--help' + begin + require 'rdoc/ri/ri_paths' + require 'rdoc/usage' + RDoc::usage + exit + rescue Exception => e + puts("Install RDoc::usage or view the comments in the top of the script to get detailed help") if e.to_str != "exit" + end + + exit + when '--server' + @server = arg + when '--port' + @port = arg + when '--user' + @user = arg + when '--password' + @password = arg + end +} + +@conn = Stomp::Connection.open(@user, @password, @server, @port, true) + +STDOUT.sync = true + +def showhelp + puts("List of commands:") + puts("\n\t- subscribe /(topic|queue)/foo subscribes to topic of queue 'foo'") + puts("\t- /(topic|queue|/foo bar sends msg 'bar' to topic of queue 'foo'") + puts("\t- quit|exit|q|^d exit") + puts("\t- detail show/dont show time and topic a msg was received on") + puts("\t- help show this help") +end + +@showdetails = true + +Thread.new(@conn) do |amq| + while true + msg = amq.receive + dest = msg.headers["destination"] + time = Time.now.strftime('%H:%M:%S') + + if @showdetails + msg = "\r#{time}:#{dest} > #{msg.body.chomp}\n" + else + msg = "\r#{msg.body.chomp}\n" + end + + puts (msg) + end +end + +loop do + line = Readline::readline('AMQ> ') + if line + Readline::HISTORY.push(line) if line != "" + else + exit + end + + if (line =~ /^(\/(topic|queue)\/\S+)\s+(.+)$/) + puts("Sending '#{$3}' to #{$1}") + + if @conn.respond_to?("publish") + @conn.publish($1, $3) + else + @conn.send($1, $3) + end + + elsif (line =~ /^sub\S* (\/(topic|queue)\/\S+)$/) + puts("Subscribing to #{$1}") + + @conn.subscribe($1) + elsif (line =~ /^det(ail)*$/) + if @showdetails + @showdetails = false + puts("No longer showing details") + else + @showdetails = true + puts("Showing time and topic for each msg") + end + elsif (line =~ /^(quit|exit|q)$/) + exit + elsif (line =~ /^(help|h|\?)$/) + showhelp + elsif (line =~ /^$/) + else + puts("ERROR: unrecognised input: #{line}") + end +end diff --git a/ext/vim/_.snippets b/ext/vim/_.snippets new file mode 100644 index 0000000..8ba2f3e --- /dev/null +++ b/ext/vim/_.snippets @@ -0,0 +1,10 @@ +snippet mcagent + module MCollective + module Agent + class ${1:Agentname} for additions and feedback, +snippet meta + metadata :name => "${1:`Filename('', 'name')`}", + :description => "${2:description}", + :author => "${3:`g:snips_author`}", + :license => "${4:license}", + :version => "${5:version}", + :url => "${6:homepage}", + :timeout => ${7:run timeout} + + ${8} +snippet discovery + discovery do + capabilities ${1:capability list} + end +snippet dataquery + dataquery :description => "${1:data query description}" do + ${2} + end +snippet action + action "${1:action name}", :description => "${2:action description}" do + ${3} + end +snippet input String + input :${1:input name}, + :prompt => "${2:prompt when asking for information}", + :description => "${3:description of the input}", + :type => :string, + :validation => '${4:^.+$}', + :optional => ${5:false}, + :maxlength => ${6:20} + + ${7} +snippet input List + input :${1:input name}, + :prompt => "${2:prompt when asking for information}", + :description => "${3:description of the input}", + :type => :list, + :optional => ${4:false}, + :list => [${5:list members}] + + ${6} +snippet input Numeric + input :${1:input name}, + :prompt => "${2:prompt when asking for information}", + :description => "${3:description of the input}", + :type => :number, + :optional => ${4:false} + + ${5} +snippet input Boolean + input :${1:input name}, + :prompt => "${2:prompt when asking for information}", + :description => "${3:description of the input}", + :type => :boolean, + :optional => ${4:false} + + ${5} +snippet output + output ${1:output name}, + :description => "${2:description of this output data}", + :display_as => "${3:what do display}", + :default => ${4:nil} + + ${5} +snippet display Always + display :always + + +snippet display Only OK results + display :ok + + +snippet display Only failed results + display :failed + + diff --git a/ext/windows/README.md b/ext/windows/README.md new file mode 100644 index 0000000..6762fba --- /dev/null +++ b/ext/windows/README.md @@ -0,0 +1,34 @@ +These files support installing and using mcollective on MS Windows. + +Here are a few instructions for people who wish to do early adopter +testing, before 2.0 is out we hope to have this packaged into a msi +installer but your early feedback will help. + +Assuming you are installing mcollective into C:\marionette-collective: + + * Install Ruby from http://rubyinstaller.org/, use 1.8.7 + * Install the following gems: stomp, win32-process, win32-service, + sys-admin, windows-api + * extract the zip file or clone the git repo into C:\marionette-collective + * copy the files from C:\marionette-collective\ext\windows\*.* into + C:\marionette-collective\bin + * Install any plugins and their dependencies into C:\marionette-collective\plugins + specifically for the package and service agents you can install Puppet via gems + * Edit the configuration files setting: + * libdir = c:\marionette-collective\plugins + * logfile = c:\marionette-collective\mcollective.log + * plugin.yaml = c:\marionette-collective\etc\facts.ysml + * daemonize = 1 + * change directories to c:\marionette-collective\bin and run register_service.bat + +At this point you would have your service registered into the windows service +manager but set to manual start. If you start it there it should run ok. + +If it does not run: + + * Look in the log files, set it to debug level + * If the log files are empty look at the command the service wrapper runs + and run it by hand. This will show you any early exception preventing it + from running. It wont succesfully start but you should see why it does + not get far enough to start writing logs. + diff --git a/ext/windows/environment.bat b/ext/windows/environment.bat new file mode 100644 index 0000000..a0a372e --- /dev/null +++ b/ext/windows/environment.bat @@ -0,0 +1,16 @@ +SET BASEDIR=%~dp0.. +SET BASEDIR=%BASEDIR:\bin\..=% + +SET SERVER_CONFIG=%BASEDIR%\etc\server.cfg +SET CLIENT_CONFIG=%BASEDIR%\etc\client.cfg + +SET MCOLLECTIVED=%BASEDIR%\bin\mcollectived +SET MC_STARTTYPE=manual +REM SET MC_STARTTYPE=auto + +SET PATH=%BASEDIR%\bin;%PATH% + +SET RUBYLIB=%BASEDIR%\lib;%RUBYLIB% +SET RUBYLIB=%RUBYLIB:\=/% + +SET RUBY="ruby" diff --git a/ext/windows/mco.bat b/ext/windows/mco.bat new file mode 100644 index 0000000..c4fb24e --- /dev/null +++ b/ext/windows/mco.bat @@ -0,0 +1,7 @@ +@echo off + +SETLOCAL + +call "%~dp0environment.bat" %0 %* + +%RUBY% -S -- mco %* --config "%CLIENT_CONFIG%" diff --git a/ext/windows/register_service.bat b/ext/windows/register_service.bat new file mode 100644 index 0000000..b5b15d6 --- /dev/null +++ b/ext/windows/register_service.bat @@ -0,0 +1,7 @@ +@echo off + +SETLOCAL + +call "%~dp0environment.bat" %0 %* + +%RUBY% -S -- service_manager.rb --install diff --git a/ext/windows/service_manager.rb b/ext/windows/service_manager.rb new file mode 100644 index 0000000..8b24892 --- /dev/null +++ b/ext/windows/service_manager.rb @@ -0,0 +1,94 @@ +require 'optparse' + +opt = OptionParser.new + +ruby_path = ENV["RUBY"].gsub('"','') +basedir = ENV["BASEDIR"] +libdir = ENV["RUBYLIB"] +mcollectived = ENV["MCOLLECTIVED"] +configfile = ENV["SERVER_CONFIG"] + +unless File.exist?(ruby_path) + ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| + ruby = File.join(path, "ruby.exe") + + if File.exist?(ruby) + ruby_path = ruby + break + end + end +end + +abort("Can't find ruby.ext in the path") unless ruby_path + +options = {:name => "mcollectived", + :display_name => "The Marionette Collective", + :description => "Puppet Labs server orchestration framework", + :command => '%s -I"%s" -- "%s" --config "%s"' % [ ruby_path, libdir, mcollectived, configfile ]} + +action = false + +opt.on("--install", "Install service") do + action = :install +end + +opt.on("--uninstall", "Remove service") do + action = :uninstall +end + +opt.on("--name NAME", String, "Service name (#{options[:name]})") do |n| + options[:name] = n +end + +opt.on("--description DESCRIPTION", String, "Service description (#{options[:description]})") do |v| + options[:description] = v +end + +opt.on("--display NAME", String, "Service display name (#{options[:display_name]})") do |n| + options[:display_name] = n +end + +opt.on("--command COMMAND", String, "Service command (#{options[:command]})") do |c| + options[:command] = c +end + +opt.parse! + +abort "Please choose an action with --install or --uninstall" unless action + +require 'rubygems' +require 'win32/service' + +include Win32 + +case action + when :install + if ENV["MC_STARTTYPE"] =~ /auto/i + start_type = Service::AUTO_START + else + start_type = Service::DEMAND_START + end + + Service.new( + :service_name => options[:name], + :display_name => options[:display_name], + :description => options[:description], + :binary_path_name => options[:command], + :service_type => Service::SERVICE_WIN32_OWN_PROCESS, + :start_type => start_type + ) + + puts "Service %s installed" % [options[:name]] + + when :uninstall + Service.stop(options[:name]) unless Service.status(options[:name]).current_state == 'stopped' + + while Service.status(options[:name]).current_state != 'stopped' + puts "Waiting for service %s to stop" % [options[:name]] + sleep 1 + end + + Service.delete(options[:name]) + + puts "Service %s removed" % [options[:name]] +end diff --git a/ext/windows/unregister_service.bat b/ext/windows/unregister_service.bat new file mode 100644 index 0000000..cf78ad7 --- /dev/null +++ b/ext/windows/unregister_service.bat @@ -0,0 +1,7 @@ +@echo off + +SETLOCAL + +call "%~dp0environment.bat" %0 %* + +%RUBY% -S -- service_manager.rb --uninstall diff --git a/ext/zsh/_mco b/ext/zsh/_mco new file mode 100644 index 0000000..2b2fb0a --- /dev/null +++ b/ext/zsh/_mco @@ -0,0 +1,94 @@ +#compdef mco + +# completion for the mcollective cli. +# +# for the main mco application it will complete +# the list of available applications +# +# for the rpc application it will complete first +# the list of agents, then actions and then each +# input. +# +# For all other applications it will just complete +# the common command line options, to add another +# application simply define a function called +# _mco_application_foo() for the foo application + +_mco() { + if (( CURRENT > 2 )); then + local application=${words[2]} + + shift words + + args=({-W,--with}'[Combined class and fact filter]' \ + {-S,--select}'[Select filter]' \ + {-F,--wf,--with-fact}'[Fact filter]' \ + {-C,--wc,--with-class}'[Class filter]' \ + {-A,--wa,--with-agent}'[Agent filter]' \ + {-I,--wi,--with-identity}'[Identity filter]' \ + {-T,--target}'[Target collective]' \ + {--dm,--disc-method}'[Which discovery method to use]' \ + {--do,--disc-option}'[Options to pass to the discovery method]' \ + {--dt,--discovery-timeout}'[Discovery timeout]' \ + {-t,--timeout}'[Command Timeout]' \ + {-q,--quiet}'[Surpress verbose output]' \ + {-c,--config}'[Path to the config file]' \ + {-v,--verbose}'[Be verbose]' \ + {-h,--help}'[Show complete help message]' \ + '--nodes[List of nodes to address]' \ + '--ttl[Time To Live for the request]' \ + '--reply-to[Custom reply target]') + + curcontext="${curcontext%:*:*}:mco-${application}" + + if (( $+functions[_mco_application_$application] > 0 ));then + _mco_application_$application + fi + + _arguments -s : $args + else + local -a cmdlist + _call_program mco-list-applications mco completion --list-applications -v | while read -A hline; do + cmdlist=($cmdlist "${hline}") + done + + curcontext="${curcontext%:*:*}:mco-applications" + + _describe -t mco-application 'MCollective applications' cmdlist + fi +} + +_mco_application_rpc() { + local -a clist + + if (( CURRENT == 3 )); then + _call_program mco-list-agents mco completion --list-agents -v | while read -A hline; do + clist=($clist "${hline}") + done + + _describe -t mco-agents "MCollective agents" clist + elif (( CURRENT == 4 )); then + _call_program mco-list-actions mco completion --list-actions --agent=${words[2]} -v | while read -A hline; do + clist=($clist "${hline}") + done + + _describe -t mco-actions "${words[2]} actions" clist + + elif (( CURRENT > 4 )); then + _call_program mco-list-inputs mco completion --list-inputs --action=${words[3]} --agent=${words[2]} -v | while read hline; do + clist=($clist $hline) + done + + _describe -t mco-inputs "${words[3]} inputs" clist -S = + fi + + args+=( + {--np,--no-progress}'[Do not show the progress bar]' \ + {--nr,--no-results}'[Do not process results, just send request]' \ + {-1,--one}'[Send request to only one discovered node]' \ + '--batch[Do request in batches]' \ + '--batch-sleep[Sleep time between batches]' \ + {--ln,--limit-nodes}'[Only send the request to a certain number of discovered nodes]' \ + {-j,--json}'[Output result as JSON data]' + ) +} diff --git a/lib/mcollective.rb b/lib/mcollective.rb new file mode 100644 index 0000000..8b376df --- /dev/null +++ b/lib/mcollective.rb @@ -0,0 +1,67 @@ +require 'rubygems' +require 'stomp' +require 'timeout' +require 'digest/md5' +require 'optparse' +require 'singleton' +require 'socket' +require 'erb' +require 'shellwords' +require 'rbconfig' +require 'tempfile' +require 'tmpdir' +require 'mcollective/exception' +require 'mcollective/monkey_patches' +require "mcollective/translatable" +require 'mcollective/cache' + +# == The Marionette Collective +# +# Framework to build and run Systems Administration agents running on a +# publish/subscribe middleware system. The system allows you to treat your +# network as the only true source of the state of your platform via discovery agents +# and allow you to run agents matching discovery criteria. +# +# For an overview of the idea behind this and what it enables please see: +# +# http://www.devco.net/archives/2009/10/18/middleware_for_systems_administration.php +module MCollective + autoload :Agent, "mcollective/agent" + autoload :Agents, "mcollective/agents" + autoload :Aggregate, "mcollective/aggregate" + autoload :Application, "mcollective/application" + autoload :Applications, "mcollective/applications" + autoload :Client, "mcollective/client" + autoload :Config, "mcollective/config" + autoload :Connector, "mcollective/connector" + autoload :Data, "mcollective/data" + autoload :DDL, "mcollective/ddl" + autoload :Discovery, "mcollective/discovery" + autoload :Facts, "mcollective/facts" + autoload :Logger, "mcollective/logger" + autoload :Log, "mcollective/log" + autoload :Matcher, "mcollective/matcher" + autoload :Message, "mcollective/message" + autoload :Optionparser, "mcollective/optionparser" + autoload :Generators, "mcollective/generators" + autoload :PluginManager, "mcollective/pluginmanager" + autoload :PluginPackager, "mcollective/pluginpackager" + autoload :Registration, "mcollective/registration" + autoload :RPC, "mcollective/rpc" + autoload :Runner, "mcollective/runner" + autoload :RunnerStats, "mcollective/runnerstats" + autoload :Security, "mcollective/security" + autoload :Shell, "mcollective/shell" + autoload :SSL, "mcollective/ssl" + autoload :Util, "mcollective/util" + autoload :Validator, "mcollective/validator" + autoload :Vendor, "mcollective/vendor" + + MCollective::Vendor.load_vendored + + VERSION="@DEVELOPMENT_VERSION@" + + def self.version + VERSION + end +end diff --git a/lib/mcollective/agent.rb b/lib/mcollective/agent.rb new file mode 100644 index 0000000..204aa6d --- /dev/null +++ b/lib/mcollective/agent.rb @@ -0,0 +1,5 @@ +module MCollective + module Agent + + end +end diff --git a/lib/mcollective/agents.rb b/lib/mcollective/agents.rb new file mode 100644 index 0000000..923d47a --- /dev/null +++ b/lib/mcollective/agents.rb @@ -0,0 +1,149 @@ +module MCollective + # A collection of agents, loads them, reloads them and dispatches messages to them. + # It uses the PluginManager to store, load and manage instances of plugins. + class Agents + def initialize(agents = {}) + @config = Config.instance + raise ("Configuration has not been loaded, can't load agents") unless @config.configured + + @@agents = agents + + loadagents + end + + # Deletes all agents + def clear! + @@agents.each_key do |agent| + PluginManager.delete "#{agent}_agent" + Util.unsubscribe(Util.make_subscriptions(agent, :broadcast)) + end + + @@agents = {} + end + + # Loads all agents from disk + def loadagents + Log.debug("Reloading all agents from disk") + + clear! + + @config.libdir.each do |libdir| + agentdir = "#{libdir}/mcollective/agent" + next unless File.directory?(agentdir) + + Dir.new(agentdir).grep(/\.rb$/).each do |agent| + agentname = File.basename(agent, ".rb") + loadagent(agentname) unless PluginManager.include?("#{agentname}_agent") + end + end + end + + # Loads a specified agent from disk if available + def loadagent(agentname) + agentfile = findagentfile(agentname) + return false unless agentfile + classname = class_for_agent(agentname) + + PluginManager.delete("#{agentname}_agent") + + begin + single_instance = ["registration", "discovery"].include?(agentname) + + PluginManager.loadclass(classname) + + if activate_agent?(agentname) + PluginManager << {:type => "#{agentname}_agent", :class => classname, :single_instance => single_instance} + + # Attempt to instantiate the agent once so any validation and hooks get run + # this does a basic sanity check on the agent as a whole, if this fails it + # will be removed from the plugin list + PluginManager["#{agentname}_agent"] + + Util.subscribe(Util.make_subscriptions(agentname, :broadcast)) unless @@agents.include?(agentname) + + @@agents[agentname] = {:file => agentfile} + return true + else + Log.debug("Not activating agent #{agentname} due to agent policy in activate? method") + return false + end + rescue Exception => e + Log.error("Loading agent #{agentname} failed: #{e}") + PluginManager.delete("#{agentname}_agent") + return false + end + end + + # Builds a class name string given a Agent name + def class_for_agent(agent) + "MCollective::Agent::#{agent.capitalize}" + end + + # Checks if a plugin should be activated by + # calling #activate? on it if it responds to + # that method else always activate it + def activate_agent?(agent) + klass = Kernel.const_get("MCollective").const_get("Agent").const_get(agent.capitalize) + + if klass.respond_to?("activate?") + return klass.activate? + else + Log.debug("#{klass} does not have an activate? method, activating as default") + return true + end + rescue Exception => e + Log.warn("Agent activation check for #{agent} failed: #{e.class}: #{e}") + return false + end + + # searches the libdirs for agents + def findagentfile(agentname) + @config.libdir.each do |libdir| + agentfile = File.join([libdir, "mcollective", "agent", "#{agentname}.rb"]) + if File.exist?(agentfile) + Log.debug("Found #{agentname} at #{agentfile}") + return agentfile + end + end + return false + end + + # Determines if we have an agent with a certain name + def include?(agentname) + PluginManager.include?("#{agentname}_agent") + end + + # Dispatches a message to an agent, accepts a block that will get run if there are + # any replies to process from the agent + def dispatch(request, connection) + Log.debug("Dispatching a message to agent #{request.agent}") + + Thread.new do + begin + agent = PluginManager["#{request.agent}_agent"] + + Timeout::timeout(agent.timeout) do + replies = agent.handlemsg(request.payload, connection) + + # Agents can decide if they wish to reply or not, + # returning nil will mean nothing goes back to the + # requestor + unless replies == nil + yield(replies) + end + end + rescue Timeout::Error => e + Log.warn("Timeout while handling message for #{request.agent}") + rescue Exception => e + Log.error("Execution of #{request.agent} failed: #{e}") + Log.error(e.backtrace.join("\n\t\t")) + end + end + end + + # Get a list of agents that we have + def self.agentlist + @@agents.keys + end + end +end diff --git a/lib/mcollective/aggregate.rb b/lib/mcollective/aggregate.rb new file mode 100644 index 0000000..fa7f60d --- /dev/null +++ b/lib/mcollective/aggregate.rb @@ -0,0 +1,85 @@ +module MCollective + class Aggregate + autoload :Result, 'mcollective/aggregate/result' + autoload :Base, 'mcollective/aggregate/base' + + attr_accessor :ddl, :functions, :action, :failed + + def initialize(ddl) + @functions = [] + @ddl = ddl + @action = ddl[:action] + @failed = [] + + create_functions + end + + # Creates instances of the Aggregate functions and stores them in the function array. + # All aggregate call and summarize method calls operate on these function as a batch. + def create_functions + @ddl[:aggregate].each_with_index do |agg, i| + output = agg[:args][0] + + if contains_output?(output) + arguments = agg[:args][1] + format = (arguments.delete(:format) if arguments) || nil + begin + @functions << load_function(agg[:function]).new(output, arguments, format, @action) + rescue Exception => e + Log.error("Cannot create aggregate function '#{output}'. #{e.to_s}") + @failed << {:name => output, :type => :startup} + end + else + Log.error("Cannot create aggregate function '#{output}'. '#{output}' has not been specified as a valid ddl output.") + @failed << {:name => output, :type => :create} + end + end + end + + # Check if the function param is defined as an output for the action in the ddl + def contains_output?(output) + @ddl[:output].keys.include?(output) + end + + # Call all the appropriate functions with the reply data received from RPC::Client + def call_functions(reply) + @functions.each do |function| + Log.debug("Calling aggregate function #{function} for result") + begin + function.process_result(reply[:data][function.output_name], reply) + rescue Exception => e + Log.error("Could not process aggregate function for '#{function.output_name}'. #{e.to_s}") + @failed << {:name => function.output_name, :type => :process_result} + @functions.delete(function) + end + end + end + + # Finalizes the function returning a result object + def summarize + summary = @functions.map do |function| + begin + function.summarize + rescue Exception => e + Log.error("Could not summarize aggregate result for '#{function.output_name}'. #{e.to_s}") + @failed << {:name => function.output_name, :type => :summarize} + nil + end + end + + summary.reject{|x| x.nil?}.sort do |x,y| + x.result[:output] <=> y.result[:output] + end + end + + # Loads function from disk for use + def load_function(function_name) + function_name = function_name.to_s.capitalize + + PluginManager.loadclass("MCollective::Aggregate::#{function_name}") unless Aggregate.const_defined?(function_name) + Aggregate.const_get(function_name) + rescue Exception + raise "Aggregate function file '#{function_name.downcase}.rb' cannot be loaded" + end + end +end diff --git a/lib/mcollective/aggregate/base.rb b/lib/mcollective/aggregate/base.rb new file mode 100644 index 0000000..5a59a91 --- /dev/null +++ b/lib/mcollective/aggregate/base.rb @@ -0,0 +1,40 @@ +module MCollective + class Aggregate + class Base + attr_accessor :name, :result, :output_name, :action, :aggregate_format, :arguments + + def initialize(output_name, arguments, aggregate_format, action) + @name = self.class.to_s + @output_name = output_name + + # Any additional arguments passed in the ddl after the output field will + # be stored in the arguments array which can be used in the function + @arguments = arguments + @aggregate_format = aggregate_format + @action = action + @result = {:value => nil, :type => nil, :output => output_name} + + startup_hook + end + + ['startup_hook', 'process_result'].each do |method| + define_method method do + raise RuntimeError, "'#{method}' method of function class #{@name} has not yet been implemented" + end + end + + # Stops execution of the function and returns a specific ResultObject, + # aggregate functions will most likely override this but this is the simplest + # case so we might as well default to that + def summarize + raise "Result type is not set while trying to summarize aggregate function results" unless @result[:type] + + result_class(@result[:type]).new(@result, @aggregate_format, @action) + end + + def result_class(type) + Result.const_get("#{type.to_s.capitalize}Result") + end + end + end +end diff --git a/lib/mcollective/aggregate/result.rb b/lib/mcollective/aggregate/result.rb new file mode 100644 index 0000000..8734b7d --- /dev/null +++ b/lib/mcollective/aggregate/result.rb @@ -0,0 +1,9 @@ +module MCollective + class Aggregate + module Result + autoload :Base, 'mcollective/aggregate/result/base' + autoload :NumericResult, 'mcollective/aggregate/result/numeric_result' + autoload :CollectionResult, 'mcollective/aggregate/result/collection_result' + end + end +end diff --git a/lib/mcollective/aggregate/result/base.rb b/lib/mcollective/aggregate/result/base.rb new file mode 100644 index 0000000..a04427b --- /dev/null +++ b/lib/mcollective/aggregate/result/base.rb @@ -0,0 +1,25 @@ +module MCollective + class Aggregate + module Result + class Base + attr_accessor :result, :aggregate_format, :action + + def initialize(result, aggregate_format, action) + raise "No aggregate_format defined in ddl or aggregate function" unless aggregate_format + + @result = result + @aggregate_format = aggregate_format + @action = action + end + + def to_s + raise "'to_s' method not implemented for result class '#{self.class}'" + end + + def result_type + @result[:type] + end + end + end + end +end diff --git a/lib/mcollective/aggregate/result/collection_result.rb b/lib/mcollective/aggregate/result/collection_result.rb new file mode 100644 index 0000000..1ee01fc --- /dev/null +++ b/lib/mcollective/aggregate/result/collection_result.rb @@ -0,0 +1,19 @@ +module MCollective + class Aggregate + module Result + class CollectionResult y[1]}.reverse.each do |value| + result.puts @aggregate_format % [value[0], value[1]] + end + + result.string.chomp + end + end + end + end +end diff --git a/lib/mcollective/aggregate/result/numeric_result.rb b/lib/mcollective/aggregate/result/numeric_result.rb new file mode 100644 index 0000000..ffa77ba --- /dev/null +++ b/lib/mcollective/aggregate/result/numeric_result.rb @@ -0,0 +1,13 @@ +module MCollective + class Aggregate + module Result + class NumericResult "The foo option" + # :arguments => ["--foo ARG"] + # + # after this the value supplied will be in configuration[:foo] + def option(name, arguments) + opt = {:name => name, + :description => nil, + :arguments => [], + :type => String, + :required => false, + :validate => Proc.new { true }} + + arguments.each_pair{|k,v| opt[k] = v} + + self[:cli_arguments] << opt + end + + # Creates an empty set of options + def intialize_application_options + @application_options = {:description => nil, + :usage => [], + :cli_arguments => [], + :exclude_arg_sections => []} + end + end + + # The application configuration built from CLI arguments + def configuration + @application_configuration ||= {} + @application_configuration + end + + # The active options hash used for MC::Client and other configuration + def options + @options + end + + # Calls the supplied block in an option for validation, an error raised + # will log to STDERR and exit the application + def validate_option(blk, name, value) + validation_result = blk.call(value) + + unless validation_result == true + STDERR.puts "Validation of #{name} failed: #{validation_result}" + exit 1 + end + end + + # Creates a standard options hash, pass in a block to add extra headings etc + # see Optionparser + def clioptions(help) + oparser = Optionparser.new({:verbose => false, :progress_bar => true}, "filter", application_options[:exclude_arg_sections]) + + options = oparser.parse do |parser, options| + if block_given? + yield(parser, options) + end + + RPC::Helpers.add_simplerpc_options(parser, options) unless application_options[:exclude_arg_sections].include?("rpc") + end + + return oparser.parser.help if help + + validate_cli_options + + post_option_parser(configuration) if respond_to?(:post_option_parser) + + return options + rescue Exception => e + application_failure(e) + end + + # Builds an ObjectParser config, parse the CLI options and validates based + # on the option config + def application_parse_options(help=false) + @options ||= {:verbose => false} + + @options = clioptions(help) do |parser, options| + parser.define_head application_description if application_description + parser.banner = "" + + if application_usage + parser.separator "" + + application_usage.each do |u| + parser.separator "Usage: #{u}" + end + + parser.separator "" + end + + parser.separator "Application Options" unless application_cli_arguments.empty? + + parser.define_tail "" + parser.define_tail "The Marionette Collective #{MCollective.version}" + + + application_cli_arguments.each do |carg| + opts_array = [] + + opts_array << :on + + # if a default is set from the application set it up front + if carg.include?(:default) + configuration[carg[:name]] = carg[:default] + end + + # :arguments are multiple possible ones + if carg[:arguments].is_a?(Array) + carg[:arguments].each {|a| opts_array << a} + else + opts_array << carg[:arguments] + end + + # type was given and its not one of our special types, just pass it onto optparse + opts_array << carg[:type] if carg[:type] && ![:boolean, :bool, :array].include?(carg[:type]) + + opts_array << carg[:description] + + # Handle our special types else just rely on the optparser to handle the types + if [:bool, :boolean].include?(carg[:type]) + parser.send(*opts_array) do |v| + validate_option(carg[:validate], carg[:name], v) + + configuration[carg[:name]] = v + end + + elsif carg[:type] == :array + parser.send(*opts_array) do |v| + validate_option(carg[:validate], carg[:name], v) + + configuration[carg[:name]] = [] unless configuration.include?(carg[:name]) + configuration[carg[:name]] << v + end + + else + parser.send(*opts_array) do |v| + validate_option(carg[:validate], carg[:name], v) + + configuration[carg[:name]] = v + end + end + end + end + end + + def validate_cli_options + # Check all required parameters were set + validation_passed = true + application_cli_arguments.each do |carg| + # Check for required arguments + if carg[:required] + unless configuration[ carg[:name] ] + validation_passed = false + STDERR.puts "The #{carg[:name]} option is mandatory" + end + end + end + + unless validation_passed + STDERR.puts "\nPlease run with --help for detailed help" + exit 1 + end + + + end + + # Retrieves the full hash of application options + def application_options + self.class.application_options + end + + # Retrieve the current application description + def application_description + application_options[:description] + end + + # Return the current usage text false if nothing is set + def application_usage + usage = application_options[:usage] + + usage.empty? ? false : usage + end + + # Returns an array of all the arguments built using + # calls to optin + def application_cli_arguments + application_options[:cli_arguments] + end + + # Handles failure, if we're far enough in the initialization + # phase it will log backtraces if its in verbose mode only + def application_failure(e, err_dest=STDERR) + # peole can use exit() anywhere and not get nasty backtraces as a result + if e.is_a?(SystemExit) + disconnect + raise(e) + end + + if e.is_a?(CodedError) + err_dest.puts "\nThe %s application failed to run: %s: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:bold, e.code), Util.colorize(:red, e.to_s)] + + err_dest.puts + if options[:verbose] + err_dest.puts "Use the 'mco doc %s' command for details about this error" % e.code + else + err_dest.puts "Use the 'mco doc %s' command for details about this error, use -v for full error backtrace details" % e.code + end + else + if options[:verbose] + err_dest.puts "\nThe %s application failed to run: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:red, e.to_s)] + else + err_dest.puts "\nThe %s application failed to run, use -v for full error backtrace details: %s\n" % [ Util.colorize(:bold, $0), Util.colorize(:red, e.to_s)] + end + end + + if options.nil? || options[:verbose] + e.backtrace.first << Util.colorize(:red, " <----") + err_dest.puts "\n%s %s" % [ Util.colorize(:red, e.to_s), Util.colorize(:bold, "(#{e.class.to_s})")] + e.backtrace.each{|l| err_dest.puts "\tfrom #{l}"} + end + + disconnect + + exit 1 + end + + def help + application_parse_options(true) + end + + # The main logic loop, builds up the options, validate configuration and calls + # the main as supplied by the user. Disconnects when done and pass any exception + # onto the application_failure helper + def run + application_parse_options + + validate_configuration(configuration) if respond_to?(:validate_configuration) + + Util.setup_windows_sleeper if Util.windows? + + main + + disconnect + + rescue Exception => e + application_failure(e) + end + + def disconnect + MCollective::PluginManager["connector_plugin"].disconnect + rescue + end + + # Fake abstract class that logs if the user tries to use an application without + # supplying a main override method. + def main + STDERR.puts "Applications need to supply a 'main' method" + exit 1 + end + + # A helper that creates a consistent exit code for applications by looking at an + # instance of MCollective::RPC::Stats + # + # Exit with 0 if nodes were discovered and all passed + # Exit with 0 if no discovery were done and > 0 responses were received + # Exit with 1 if no nodes were discovered + # Exit with 2 if nodes were discovered but some RPC requests failed + # Exit with 3 if nodes were discovered, but not responses received + # Exit with 4 if no discovery were done and no responses were received + def halt(stats) + request_stats = {:discoverytime => 0, + :discovered => 0, + :failcount => 0}.merge(stats.to_hash) + + # was discovery done? + if request_stats[:discoverytime] != 0 + # was any nodes discovered + if request_stats[:discovered] == 0 + exit 1 + + # nodes were discovered, did we get responses + elsif request_stats[:responses] == 0 + exit 3 + + else + # we got responses and discovery was done, no failures + if request_stats[:failcount] == 0 + exit 0 + else + exit 2 + end + end + else + # discovery wasnt done and we got no responses + if request_stats[:responses] == 0 + exit 4 + else + exit 0 + end + end + end + + # Wrapper around MC::RPC#rpcclient that forcably supplies our options hash + # if someone forgets to pass in options in an application the filters and other + # cli options wouldnt take effect which could have a disasterous outcome + def rpcclient(agent, flags = {}) + flags[:options] = options unless flags.include?(:options) + flags[:exit_on_failure] = false + + super + end + end +end diff --git a/lib/mcollective/applications.rb b/lib/mcollective/applications.rb new file mode 100644 index 0000000..3be93f5 --- /dev/null +++ b/lib/mcollective/applications.rb @@ -0,0 +1,134 @@ +module MCollective + class Applications + def self.[](appname) + load_application(appname) + PluginManager["#{appname}_application"] + end + + def self.run(appname) + load_config + + begin + load_application(appname) + rescue Exception => e + e.backtrace.first << Util.colorize(:red, " <----") + STDERR.puts "Application '#{appname}' failed to load:" + STDERR.puts + STDERR.puts Util.colorize(:red, " #{e} (#{e.class})") + STDERR.puts + STDERR.puts " %s" % [e.backtrace.join("\n ")] + exit 1 + end + + PluginManager["#{appname}_application"].run + end + + def self.load_application(appname) + return if PluginManager.include?("#{appname}_application") + + load_config + + PluginManager.loadclass "MCollective::Application::#{appname.capitalize}" + PluginManager << {:type => "#{appname}_application", :class => "MCollective::Application::#{appname.capitalize}"} + end + + # Returns an array of applications found in the lib dirs + def self.list + load_config + + PluginManager.find("application") + rescue SystemExit + exit 1 + rescue Exception => e + STDERR.puts "Failed to generate application list: #{e.class}: #{e}" + exit 1 + end + + # Filters a string of opts out using Shellwords + # keeping only things related to --config and -c + def self.filter_extra_options(opts) + res = "" + words = Shellwords.shellwords(opts) + words.each_with_index do |word,idx| + if word == "-c" + return "--config=#{words[idx + 1]}" + elsif word == "--config" + return "--config=#{words[idx + 1]}" + elsif word =~ /\-c=/ + return word + elsif word =~ /\-\-config=/ + return word + end + end + + return "" + end + + # We need to know the config file in order to know the libdir + # so that we can find applications. + # + # The problem is the CLI might be stuffed with options only the + # app in the libdir might understand so we have a chicken and + # egg situation. + # + # We're parsing and filtering MCOLLECTIVE_EXTRA_OPTS removing + # all but config related options and parsing the options looking + # just for the config file. + # + # We're handling failures gracefully and finally restoring the + # ARG and MCOLLECTIVE_EXTRA_OPTS to the state they were before + # we started parsing. + # + # This is mostly a hack, when we're redoing how config works + # this stuff should be made less sucky + def self.load_config + return if Config.instance.configured + + original_argv = ARGV.clone + original_extra_opts = ENV["MCOLLECTIVE_EXTRA_OPTS"].clone rescue nil + configfile = nil + + parser = OptionParser.new + parser.on("--config CONFIG", "-c", "Config file") do |f| + configfile = f + end + + parser.program_name = $0 + + parser.on("--help") + + # avoid option parsers own internal version handling that sux + parser.on("-v", "--verbose") + + if original_extra_opts + begin + # optparse will parse the whole ENV in one go and refuse + # to play along with the retry trick I do below so in + # order to handle unknown options properly I parse out + # only -c and --config deleting everything else and + # then restore the environment variable later when I + # am done with it + ENV["MCOLLECTIVE_EXTRA_OPTS"] = filter_extra_options(ENV["MCOLLECTIVE_EXTRA_OPTS"].clone) + parser.environment("MCOLLECTIVE_EXTRA_OPTS") + rescue Exception => e + Log.error("Failed to parse MCOLLECTIVE_EXTRA_OPTS: #{e}") + end + + ENV["MCOLLECTIVE_EXTRA_OPTS"] = original_extra_opts.clone + end + + begin + parser.parse! + rescue OptionParser::InvalidOption => e + retry + end + + ARGV.clear + original_argv.each {|a| ARGV << a} + + configfile = Util.config_file_for_user unless configfile + + Config.instance.loadconfig(configfile) + end + end +end diff --git a/lib/mcollective/cache.rb b/lib/mcollective/cache.rb new file mode 100644 index 0000000..50432d2 --- /dev/null +++ b/lib/mcollective/cache.rb @@ -0,0 +1,149 @@ +module MCollective + # A class to manage a number of named caches. Each cache can have a unique + # timeout for keys in it and each cache has an independent Mutex protecting + # access to it. + # + # This cache is setup early in the process of loading the mcollective + # libraries, before any threads are created etc making it suitable as a + # cross thread cache or just a store for Mutexes you need to use across + # threads like in an Agent or something. + # + # # sets up a new cache, noop if it already exist + # Cache.setup(:ddl, 600) + # => true + # + # # writes an item to the :ddl cache, this item will + # # be valid on the cache for 600 seconds + # Cache.write(:ddl, :something, "value") + # => "value" + # + # # reads from the cache, read failures due to non existing + # # data or expired data will raise an exception + # Cache.read(:ddl, :something) + # => "value" + # + # # time left till expiry, raises if nothing is found + # Cache.ttl(:ddl, :something) + # => 500 + # + # # forcibly evict something from the cache + # Cache.invalidate!(:ddl, :something) + # => "value" + # + # # deletes an entire named cache and its mutexes + # Cache.delete!(:ddl) + # => true + # + # # you can also just use this cache as a global mutex store + # Cache.setup(:mylock) + # + # Cache.synchronize(:mylock) do + # do_something() + # end + # + module Cache + extend Translatable + + # protects access to @cache_locks and top level @cache + @locks_mutex = Mutex.new + + # stores a mutex per named cache + @cache_locks = {} + + # the named caches protected by items in @cache_locks + @cache = {} + + def self.setup(cache_name, ttl=300) + @locks_mutex.synchronize do + break if @cache_locks.include?(cache_name) + + @cache_locks[cache_name] = Mutex.new + + @cache_locks[cache_name].synchronize do + @cache[cache_name] = {:max_age => Float(ttl)} + end + end + + true + end + + def self.check_cache!(cache_name) + raise_code(:PLMC13, "Could not find a cache called '%{cache_name}'", :debug, :cache_name => cache_name) unless has_cache?(cache_name) + end + + def self.has_cache?(cache_name) + @locks_mutex.synchronize do + @cache.include?(cache_name) + end + end + + def self.delete!(cache_name) + check_cache!(cache_name) + + @locks_mutex.synchronize do + @cache_locks.delete(cache_name) + @cache.delete(cache_name) + end + + true + end + + def self.write(cache_name, key, value) + check_cache!(cache_name) + + @cache_locks[cache_name].synchronize do + @cache[cache_name][key] ||= {} + @cache[cache_name][key][:cache_create_time] = Time.now + @cache[cache_name][key][:value] = value + end + + value + end + + def self.read(cache_name, key) + check_cache!(cache_name) + + unless ttl(cache_name, key) > 0 + raise_code(:PLMC11, "Cache expired on '%{cache_name}' key '%{item}'", :debug, :cache_name => cache_name, :item => key) + end + + log_code(:PLMC12, "Cache hit on '%{cache_name}' key '%{item}'", :debug, :cache_name => cache_name, :item => key) + + @cache_locks[cache_name].synchronize do + @cache[cache_name][key][:value] + end + end + + def self.ttl(cache_name, key) + check_cache!(cache_name) + + @cache_locks[cache_name].synchronize do + unless @cache[cache_name].include?(key) + raise_code(:PLMC15, "No item called '%{item}' for cache '%{cache_name}'", :debug, :cache_name => cache_name, :item => key) + end + + @cache[cache_name][:max_age] - (Time.now - @cache[cache_name][key][:cache_create_time]) + end + end + + def self.synchronize(cache_name) + check_cache!(cache_name) + + raise_code(:PLMC14, "No block supplied to synchronize on cache '%{cache_name}'", :debug, :cache_name => cache_name) unless block_given? + + @cache_locks[cache_name].synchronize do + yield + end + end + + def self.invalidate!(cache_name, key) + check_cache!(cache_name) + + @cache_locks[cache_name].synchronize do + return false unless @cache[cache_name].include?(key) + + @cache[cache_name].delete(key) + end + end + end +end diff --git a/lib/mcollective/client.rb b/lib/mcollective/client.rb new file mode 100644 index 0000000..d77eaa5 --- /dev/null +++ b/lib/mcollective/client.rb @@ -0,0 +1,219 @@ +module MCollective + # Helpers for writing clients that can talk to agents, do discovery and so forth + class Client + attr_accessor :options, :stats, :discoverer + + def initialize(configfile) + @config = Config.instance + @config.loadconfig(configfile) unless @config.configured + + @connection = PluginManager["connector_plugin"] + @security = PluginManager["security_plugin"] + + @security.initiated_by = :client + @options = nil + @subscriptions = {} + + @discoverer = Discovery.new(self) + @connection.connect + end + + # Returns the configured main collective if no + # specific collective is specified as options + def collective + if @options[:collective].nil? + @config.main_collective + else + @options[:collective] + end + end + + # Disconnects cleanly from the middleware + def disconnect + Log.debug("Disconnecting from the middleware") + @connection.disconnect + end + + # Sends a request and returns the generated request id, doesn't wait for + # responses and doesn't execute any passed in code blocks for responses + def sendreq(msg, agent, filter = {}) + if msg.is_a?(Message) + request = msg + agent = request.agent + else + ttl = @options[:ttl] || @config.ttl + request = Message.new(msg, nil, {:agent => agent, :type => :request, :collective => collective, :filter => filter, :ttl => ttl}) + request.reply_to = @options[:reply_to] if @options[:reply_to] + end + + request.encode! + + Log.debug("Sending request #{request.requestid} to the #{request.agent} agent with ttl #{request.ttl} in collective #{request.collective}") + + subscribe(agent, :reply) unless request.reply_to + + request.publish + + request.requestid + end + + def subscribe(agent, type) + unless @subscriptions.include?(agent) + subscription = Util.make_subscriptions(agent, type, collective) + Log.debug("Subscribing to #{type} target for agent #{agent}") + + Util.subscribe(subscription) + @subscriptions[agent] = 1 + end + end + + def unsubscribe(agent, type) + if @subscriptions.include?(agent) + subscription = Util.make_subscriptions(agent, type, collective) + Log.debug("Unsubscribing #{type} target for #{agent}") + + Util.unsubscribe(subscription) + @subscriptions.delete(agent) + end + end + # Blocking call that waits for ever for a message to arrive. + # + # If you give it a requestid this means you've previously send a request + # with that ID and now you just want replies that matches that id, in that + # case the current connection will just ignore all messages not directed at it + # and keep waiting for more till it finds a matching message. + def receive(requestid = nil) + reply = nil + + begin + reply = @connection.receive + reply.type = :reply + reply.expected_msgid = requestid + + reply.decode! + + reply.payload[:senderid] = Digest::MD5.hexdigest(reply.payload[:senderid]) if ENV.include?("MCOLLECTIVE_ANON") + + raise(MsgDoesNotMatchRequestID, "Message reqid #{requestid} does not match our reqid #{reply.requestid}") unless reply.requestid == requestid + rescue SecurityValidationFailed => e + Log.warn("Ignoring a message that did not pass security validations") + retry + rescue MsgDoesNotMatchRequestID => e + Log.debug("Ignoring a message for some other client") + retry + end + + reply + end + + # Performs a discovery of nodes matching the filter passed + # returns an array of nodes + # + # An integer limit can be supplied this will have the effect + # of the discovery being cancelled soon as it reached the + # requested limit of hosts + def discover(filter, timeout, limit=0) + discovered = @discoverer.discover(filter, timeout, limit) + end + + # Send a request, performs the passed block for each response + # + # times = req("status", "mcollectived", options, client) {|resp| + # pp resp + # } + # + # It returns a hash of times and timeouts for discovery and total run is taken from the options + # hash which in turn is generally built using MCollective::Optionparser + def req(body, agent=nil, options=false, waitfor=0) + if body.is_a?(Message) + agent = body.agent + waitfor = body.discovered_hosts.size || 0 + @options = body.options + end + + @options = options if options + + stat = {:starttime => Time.now.to_f, :discoverytime => 0, :blocktime => 0, :totaltime => 0} + + timeout = @discoverer.discovery_timeout(@options[:timeout], @options[:filter]) + + STDOUT.sync = true + + hosts_responded = 0 + reqid = nil + + begin + Log.debug("Publishing request to agent %s with timeout %d" % [agent, timeout]) + + Timeout.timeout(timeout) do + reqid = sendreq(body, agent, @options[:filter]) + + loop do + resp = receive(reqid) + + hosts_responded += 1 + + yield(resp.payload) + + break if (waitfor != 0 && hosts_responded >= waitfor) + end + end + rescue Interrupt => e + rescue Timeout::Error => e + ensure + unsubscribe(agent, :reply) + end + + stat[:totaltime] = Time.now.to_f - stat[:starttime] + stat[:blocktime] = stat[:totaltime] - stat[:discoverytime] + stat[:responses] = hosts_responded + stat[:noresponsefrom] = [] + stat[:requestid] = reqid + + @stats = stat + return stat + end + + def discovered_req(body, agent, options=false) + raise "Client#discovered_req has been removed, please port your agent and client to the SimpleRPC framework" + end + + # Prints out the stats returns from req and discovered_req in a nice way + def display_stats(stats, options=false, caption="stomp call summary") + options = @options unless options + + if options[:verbose] + puts("\n---- #{caption} ----") + + if stats[:discovered] + puts(" Nodes: #{stats[:discovered]} / #{stats[:responses]}") + else + puts(" Nodes: #{stats[:responses]}") + end + + printf(" Start Time: %s\n", Time.at(stats[:starttime])) + printf(" Discovery Time: %.2fms\n", stats[:discoverytime] * 1000) + printf(" Agent Time: %.2fms\n", stats[:blocktime] * 1000) + printf(" Total Time: %.2fms\n", stats[:totaltime] * 1000) + + else + if stats[:discovered] + printf("\nFinished processing %d / %d hosts in %.2f ms\n\n", stats[:responses], stats[:discovered], stats[:blocktime] * 1000) + else + printf("\nFinished processing %d hosts in %.2f ms\n\n", stats[:responses], stats[:blocktime] * 1000) + end + end + + if stats[:noresponsefrom].size > 0 + puts("\nNo response from:\n") + + stats[:noresponsefrom].each do |c| + puts if c % 4 == 1 + printf("%30s", c) + end + + puts + end + end + end +end diff --git a/lib/mcollective/config.rb b/lib/mcollective/config.rb new file mode 100644 index 0000000..8b5e409 --- /dev/null +++ b/lib/mcollective/config.rb @@ -0,0 +1,223 @@ +module MCollective + # A pretty sucky config class, ripe for refactoring/improving + class Config + include Singleton + + attr_accessor :mode + + attr_reader :topicprefix, :daemonize, :pluginconf, :libdir, :configured + attr_reader :logfile, :keeplogs, :max_log_size, :loglevel, :logfacility + attr_reader :identity, :daemonize, :connector, :securityprovider, :factsource + attr_reader :registration, :registerinterval, :topicsep, :classesfile + attr_reader :rpcauditprovider, :rpcaudit, :configdir, :rpcauthprovider + attr_reader :rpcauthorization, :color, :configfile, :rpchelptemplate + attr_reader :rpclimitmethod, :logger_type, :fact_cache_time, :collectives + attr_reader :main_collective, :ssl_cipher, :registration_collective + attr_reader :direct_addressing, :direct_addressing_threshold, :ttl, :helptemplatedir + attr_reader :queueprefix, :default_discovery_method, :default_discovery_options + + def initialize + @configured = false + end + + def loadconfig(configfile) + set_config_defaults(configfile) + + if File.exists?(configfile) + File.open(configfile, "r").each do |line| + + # strip blank spaces, tabs etc off the end of all lines + line.gsub!(/\s*$/, "") + + unless line =~ /^#|^$/ + if (line =~ /(.+?)\s*=\s*(.+)/) + key = $1 + val = $2 + + case key + when "topicsep" + @topicsep = val + when "registration" + @registration = val.capitalize + when "registration_collective" + @registration_collective = val + when "registerinterval" + @registerinterval = val.to_i + when "collectives" + @collectives = val.split(",").map {|c| c.strip} + when "main_collective" + @main_collective = val + when "topicprefix" + @topicprefix = val + when "queueprefix" + @queueprefix = val + when "logfile" + @logfile = val + when "keeplogs" + @keeplogs = val.to_i + when "max_log_size" + @max_log_size = val.to_i + when "loglevel" + @loglevel = val + when "logfacility" + @logfacility = val + when "libdir" + paths = val.split(File::PATH_SEPARATOR) + paths.each do |path| + raise("libdir paths should be absolute paths but '%s' is relative" % path) unless Util.absolute_path?(path) + + @libdir << path + unless $LOAD_PATH.include?(path) + $LOAD_PATH << path + end + end + when "identity" + @identity = val + when "direct_addressing" + val =~ /^1|y/i ? @direct_addressing = true : @direct_addressing = false + when "direct_addressing_threshold" + @direct_addressing_threshold = val.to_i + when "color" + val =~ /^1|y/i ? @color = true : @color = false + when "daemonize" + val =~ /^1|y/i ? @daemonize = true : @daemonize = false + when "securityprovider" + @securityprovider = val.capitalize + when "factsource" + @factsource = val.capitalize + when "connector" + @connector = val.capitalize + when "classesfile" + @classesfile = val + when /^plugin.(.+)$/ + @pluginconf[$1] = val + when "rpcaudit" + val =~ /^1|y/i ? @rpcaudit = true : @rpcaudit = false + when "rpcauditprovider" + @rpcauditprovider = val.capitalize + when "rpcauthorization" + val =~ /^1|y/i ? @rpcauthorization = true : @rpcauthorization = false + when "rpcauthprovider" + @rpcauthprovider = val.capitalize + when "rpchelptemplate" + @rpchelptemplate = val + when "rpclimitmethod" + @rpclimitmethod = val.to_sym + when "logger_type" + @logger_type = val + when "fact_cache_time" + @fact_cache_time = val.to_i + when "ssl_cipher" + @ssl_cipher = val + when "ttl" + @ttl = val.to_i + when "helptemplatedir" + @helptemplatedir = val + when "default_discovery_options" + @default_discovery_options << val + when "default_discovery_method" + @default_discovery_method = val + else + raise("Unknown config parameter #{key}") + end + end + end + end + + I18n.load_path = Dir[File.expand_path(File.join(File.dirname(__FILE__), "locales", "*.yml"))] + I18n.locale = :en + + read_plugin_config_dir("#{@configdir}/plugin.d") + + raise 'Identities can only match /\w\.\-/' unless @identity.match(/^[\w\.\-]+$/) + + @configured = true + + @libdir.each {|dir| Log.warn("Cannot find libdir: #{dir}") unless File.directory?(dir)} + + if @logger_type == "syslog" + raise "The sylog logger is not usable on the Windows platform" if Util.windows? + end + + PluginManager.loadclass("Mcollective::Facts::#{@factsource}_facts") + PluginManager.loadclass("Mcollective::Connector::#{@connector}") + PluginManager.loadclass("Mcollective::Security::#{@securityprovider}") + PluginManager.loadclass("Mcollective::Registration::#{@registration}") + PluginManager.loadclass("Mcollective::Audit::#{@rpcauditprovider}") if @rpcaudit + PluginManager << {:type => "global_stats", :class => RunnerStats.new} + + Log.logmsg(:PLMC1, "The Marionette Collective version %{version} started by %{name} using config file %{config}", :info, :version => MCollective::VERSION, :name => $0, :config => configfile) + else + raise("Cannot find config file '#{configfile}'") + end + end + + def set_config_defaults(configfile) + @stomp = Hash.new + @subscribe = Array.new + @pluginconf = Hash.new + @connector = "activemq" + @securityprovider = "Psk" + @factsource = "Yaml" + @identity = Socket.gethostname + @registration = "Agentlist" + @registerinterval = 0 + @registration_collective = nil + @topicsep = "." + @topicprefix = "/topic/" + @queueprefix = "/queue/" + @classesfile = "/var/lib/puppet/state/classes.txt" + @rpcaudit = false + @rpcauditprovider = "" + @rpcauthorization = false + @rpcauthprovider = "" + @configdir = File.dirname(configfile) + @color = !Util.windows? + @configfile = configfile + @logger_type = "file" + @keeplogs = 5 + @max_log_size = 2097152 + @rpclimitmethod = :first + @libdir = Array.new + @fact_cache_time = 300 + @loglevel = "info" + @logfacility = "user" + @collectives = ["mcollective"] + @main_collective = @collectives.first + @ssl_cipher = "aes-256-cbc" + @direct_addressing = false + @direct_addressing_threshold = 10 + @default_discovery_method = "mc" + @default_discovery_options = [] + @ttl = 60 + @mode = :client + + # look in the config dir for the template so users can provide their own and windows + # with odd paths will just work more often, but fall back to old behavior if it does + # not exist + @rpchelptemplate = File.join(File.dirname(configfile), "rpc-help.erb") + @rpchelptemplate = "/etc/mcollective/rpc-help.erb" unless File.exists?(@rpchelptemplate) + @helptemplatedir = File.dirname(@rpchelptemplate) + end + + def read_plugin_config_dir(dir) + return unless File.directory?(dir) + + Dir.new(dir).each do |pluginconfigfile| + next unless pluginconfigfile =~ /^([\w]+).cfg$/ + + plugin = $1 + File.open("#{dir}/#{pluginconfigfile}", "r").each do |line| + # strip blank lines + line.gsub!(/\s*$/, "") + next if line =~ /^#|^$/ + if (line =~ /(.+?)\s*=\s*(.+)/) + key = $1 + val = $2 + @pluginconf["#{plugin}.#{key}"] = val + end + end + end + end + end +end diff --git a/lib/mcollective/connector.rb b/lib/mcollective/connector.rb new file mode 100644 index 0000000..dd6308b --- /dev/null +++ b/lib/mcollective/connector.rb @@ -0,0 +1,18 @@ +module MCollective + # Connectors take care of transporting messages between clients and agents, + # the design doesn't your middleware to be very rich in features. All it + # really needs is the ability to send and receive messages to named queues/topics. + # + # At present there are assumptions about the naming of topics and queues that is + # compatible with Stomp, ie. + # + # /topic/foo.bar/baz + # /queue/foo.bar/baz + # + # This is the only naming format that is supported, but you could replace Stomp + # with something else that supports the above, see MCollective::Connector::Stomp + # for the default connector. + module Connector + autoload :Base, "mcollective/connector/base" + end +end diff --git a/lib/mcollective/connector/base.rb b/lib/mcollective/connector/base.rb new file mode 100644 index 0000000..4eabae3 --- /dev/null +++ b/lib/mcollective/connector/base.rb @@ -0,0 +1,24 @@ +module MCollective + # Connector plugins handle the communications with the middleware, you can provide your own to speak + # to something other than Stomp, your plugins must inherit from MCollective::Connector::Base and should + # provide the following methods: + # + # connect - Creates a connection to the middleware, no arguments should get its parameters from the config + # receive - Receive data from the middleware, should act like a blocking call only returning if/when data + # was received. It should get data from all subscribed channels/topics. Individual messages + # should be returned as MCollective::Request objects with the payload provided + # publish - Takes a target and msg, should send the message to the supplied target topic or destination + # subscribe - Adds a subscription to a specific message source + # unsubscribe - Removes a subscription to a specific message source + # disconnect - Disconnects from the middleware + # + # These methods are all that's needed for a new connector protocol and should hopefully be simple + # enough to not have tied us to Stomp. + module Connector + class Base + def self.inherited(klass) + PluginManager << {:type => "connector_plugin", :class => klass.to_s} + end + end + end +end diff --git a/lib/mcollective/data.rb b/lib/mcollective/data.rb new file mode 100644 index 0000000..2d848c2 --- /dev/null +++ b/lib/mcollective/data.rb @@ -0,0 +1,91 @@ +module MCollective + module Data + autoload :Base, "mcollective/data/base" + autoload :Result, "mcollective/data/result" + + def self.load_data_sources + PluginManager.find_and_load("data") + + PluginManager.grep(/_data$/).each do |plugin| + begin + unless PluginManager[plugin].class.activate? + Log.debug("Disabling data plugin %s due to plugin activation policy" % plugin) + PluginManager.delete(plugin) + end + rescue Exception => e + Log.debug("Disabling data plugin %s due to exception #{e.class}: #{e}" % plugin) + PluginManager.delete(plugin) + end + end + end + + def self.pluginname(plugin) + plugin.to_s =~ /_data$/i ? plugin.to_s.downcase : "%s_data" % plugin.to_s.downcase + end + + def self.[](plugin) + PluginManager[pluginname(plugin)] + end + + # Data.package("httpd").architecture + def self.method_missing(method, *args) + super unless PluginManager.include?(pluginname(method)) + + PluginManager[pluginname(method)].lookup(args.first) + end + + def self.ddl(plugin) + DDL.new(pluginname(plugin), :data) + end + + def self.ddl_validate(ddl, argument) + name = ddl.meta[:name] + query = ddl.entities[:data] + + DDL.validation_fail!(:PLMC31, "No dataquery has been defined in the DDL for data plugin '%{plugin}'", :error, :plugin => name) unless query + + input = query.fetch(:input, {}) + output = query.fetch(:output, {}) + + DDL.validation_fail!(:PLMC32, "No output has been defined in the DDL for data plugin %{plugin}", :error, :plugin => name) if output.keys.empty? + + if input[:query] + return true if argument.nil? && input[:query][:optional] + + ddl.validate_input_argument(input, :query, argument) + else + DDL.validation_fail!(:PLMC33, "No data plugin argument was declared in the '%{plugin}' DDL but an input was supplied", :error, :plugin => name) if argument + return true + end + end + + def self.ddl_has_output?(ddl, output) + ddl.entities[:data][:output].include?(output.to_sym) rescue false + end + + # For an input where the DDL requests a boolean or some number + # this will convert the input to the right type where possible + # else just returns the origin input unedited + # + # if anything here goes wrong just return the input value + # this is not really the end of the world or anything since + # all that will happen is that DDL validation will fail and + # the user will get an error, no need to be too defensive here + def self.ddl_transform_input(ddl, input) + begin + type = ddl.entities[:data][:input][:query][:type] + + case type + when :boolean + return DDL.string_to_boolean(input) + + when :number, :integer, :float + return DDL.string_to_number(input) + end + rescue + end + + return input + end + end +end diff --git a/lib/mcollective/data/base.rb b/lib/mcollective/data/base.rb new file mode 100644 index 0000000..3ff8213 --- /dev/null +++ b/lib/mcollective/data/base.rb @@ -0,0 +1,67 @@ +module MCollective + module Data + class Base + attr_reader :name, :result, :ddl, :timeout + + # Register plugins that inherits base + def self.inherited(klass) + type = klass.to_s.split("::").last.downcase + + PluginManager << {:type => type, :class => klass.to_s, :single_instance => false} + end + + def initialize + @name = self.class.to_s.split("::").last.downcase + @ddl = DDL.new(@name, :data) + @result = Result.new + @timeout = @ddl.meta[:timeout] || 1 + + startup_hook + end + + def lookup(what) + ddl_validate(what) + + Log.debug("Doing data query %s for '%s'" % [ @name, what ]) + + Timeout::timeout(@timeout) do + query_data(what) + end + + @result + rescue Timeout::Error + # Timeout::Error is a inherited from Interrupt which seems a really + # strange choice, making it an equivelant of ^C and such. Catch it + # and raise something less critical that will not the runner to just + # give up the ghost + msg = "Data plugin %s timed out on query '%s'" % [@name, what] + Log.error(msg) + raise MsgTTLExpired, msg + end + + def self.query(&block) + self.module_eval { define_method("query_data", &block) } + end + + def ddl_validate(what) + Data.ddl_validate(@ddl, what) + end + + # activate_when do + # file.exist?("/usr/bin/puppet") + # end + def self.activate_when(&block) + (class << self; self; end).instance_eval do + define_method("activate?", &block) + end + end + + # Always be active unless a specific block is given with activate_when + def self.activate? + return true + end + + def startup_hook;end + end + end +end diff --git a/lib/mcollective/data/result.rb b/lib/mcollective/data/result.rb new file mode 100644 index 0000000..e19a652 --- /dev/null +++ b/lib/mcollective/data/result.rb @@ -0,0 +1,40 @@ +module MCollective + module Data + class Result + # remove some methods that might clash with commonly + # used return data to improve the effectiveness of the + # method_missing lookup strategy + undef :type if method_defined?(:type) + + def initialize + @data = {} + end + + def include?(key) + @data.include?(key.to_sym) + end + + def [](key) + @data[key.to_sym] + end + + def []=(key, val) + raise "Can only store String, Integer, Float or Boolean data but got #{val.class} for key #{key}" unless [String, Fixnum, Bignum, Float, TrueClass, FalseClass].include?(val.class) + + @data[key.to_sym] = val + end + + def keys + @data.keys + end + + def method_missing(method, *args) + key = method.to_sym + + raise NoMethodError, "undefined local variable or method `%s'" % key unless include?(key) + + @data[key] + end + end + end +end diff --git a/lib/mcollective/ddl.rb b/lib/mcollective/ddl.rb new file mode 100644 index 0000000..dc2410d --- /dev/null +++ b/lib/mcollective/ddl.rb @@ -0,0 +1,124 @@ +module MCollective + # A set of classes that helps create data description language files + # for plugins. You can define meta data, actions, input and output + # describing the behavior of your agent or other plugins + # + # DDL files are used for input validation, constructing outputs, + # producing online help, informing the various display routines and + # so forth. + # + # A sample DDL for an agent be seen below, you'd put this in your agent + # dir as .ddl + # + # metadata :name => "SimpleRPC Service Agent", + # :description => "Agent to manage services using the Puppet service provider", + # :author => "R.I.Pienaar", + # :license => "GPLv2", + # :version => "1.1", + # :url => "http://mcollective-plugins.googlecode.com/", + # :timeout => 60 + # + # action "status", :description => "Gets the status of a service" do + # display :always + # + # input :service, + # :prompt => "Service Name", + # :description => "The service to get the status for", + # :type => :string, + # :validation => '^[a-zA-Z\-_\d]+$', + # :optional => true, + # :maxlength => 30 + # + # output :status, + # :description => "The status of service", + # :display_as => "Service Status" + # end + # + # There are now many types of DDL and ultimately all pugins should have + # DDL files. The code is organized so that any plugin type will magically + # just work - they will be an instane of Base which has #metadata and a few + # common cases. + # + # For plugin types that require more specific behaviors they can just add a + # class here that inherits from Base and add their specific behavior. + # + # Base defines a specific behavior for input, output and metadata which we'd + # like to keep standard across plugin types so do not completely override the + # behavior of input. The methods are written that they will gladly store extra + # content though so you add, do not remove. See the AgentDDL class for an example + # where agents want a :required argument to be always set. + module DDL + autoload :Base, "mcollective/ddl/base" + autoload :AgentDDL, "mcollective/ddl/agentddl" + autoload :DataDDL, "mcollective/ddl/dataddl" + autoload :DiscoveryDDL, "mcollective/ddl/discoveryddl" + + extend Translatable + + # There used to be only one big nasty DDL class with a bunch of mashed + # together behaviors. It's been around for ages and we would rather not + # ask all the users to change their DDL.new calls to some other factory + # method that would have this exact same behavior. + # + # So we override the behavior of #new which is a hugely sucky thing to do + # but ultimately it's what would be least disrupting to code out there + # today. We did though change DDL to a module to make it possibly a + # little less suprising, possibly. + def self.new(*args, &blk) + load_and_cache(*args) + end + + def self.load_and_cache(*args) + Cache.setup(:ddl, 300) + + plugin = args.first + args.size > 1 ? type = args[1].to_s : type = "agent" + path = "%s/%s" % [type, plugin] + + begin + ddl = Cache.read(:ddl, path) + rescue + begin + klass = DDL.const_get("%sDDL" % type.capitalize) + rescue NameError + klass = Base + end + + ddl = Cache.write(:ddl, path, klass.new(*args)) + end + + return ddl + end + + # As we're taking arguments on the command line we need a + # way to input booleans, true on the cli is a string so this + # method will take the ddl, find all arguments that are supposed + # to be boolean and if they are the strings "true"/"yes" or "false"/"no" + # turn them into the matching boolean + def self.string_to_boolean(val) + return true if ["true", "t", "yes", "y", "1"].include?(val.downcase) + return false if ["false", "f", "no", "n", "0"].include?(val.downcase) + + raise_code(:PLMC17, "%{value} does not look like a boolean argument", :debug, :value => val) + end + + # a generic string to number function, if a number looks like a float + # it turns it into a float else an int. This is naive but should be sufficient + # for numbers typed on the cli in most cases + def self.string_to_number(val) + return val.to_f if val =~ /^\d+\.\d+$/ + return val.to_i if val =~ /^\d+$/ + + raise_code(:PLMC16, "%{value} does not look like a numeric value", :debug, :value => val) + end + + # Various DDL implementations will validate and raise on error, this is a + # utility method to correctly setup a DDLValidationError exceptions and raise them + def self.validation_fail!(code, default, level, args={}) + exception = DDLValidationError.new(code, default, level, args) + exception.set_backtrace caller + + raise exception + end + end +end diff --git a/lib/mcollective/ddl/agentddl.rb b/lib/mcollective/ddl/agentddl.rb new file mode 100644 index 0000000..5952ec0 --- /dev/null +++ b/lib/mcollective/ddl/agentddl.rb @@ -0,0 +1,208 @@ +module MCollective + module DDL + # A DDL class specific to agent plugins. + # + # A full DDL can be seen below with all the possible bells and whistles present. + # + # metadata :name => "Utilities and Helpers for SimpleRPC Agents", + # :description => "General helpful actions that expose stats and internals to SimpleRPC clients", + # :author => "R.I.Pienaar ", + # :license => "Apache License, Version 2.0", + # :version => "1.0", + # :url => "http://marionette-collective.org/", + # :timeout => 10 + # + # action "get_fact", :description => "Retrieve a single fact from the fact store" do + # display :always + # + # input :fact, + # :prompt => "The name of the fact", + # :description => "The fact to retrieve", + # :type => :string, + # :validation => '^[\w\-\.]+$', + # :optional => false, + # :maxlength => 40, + # :default => "fqdn" + # + # output :fact, + # :description => "The name of the fact being returned", + # :display_as => "Fact" + # + # output :value, + # :description => "The value of the fact", + # :display_as => "Value", + # :default => "" + # + # summarize do + # aggregate summary(:value) + # end + # end + class AgentDDL nil}) + DDL.validation_fail!(:PLMC28, "Formats supplied to aggregation functions should be a hash", :error) unless format.is_a?(Hash) + DDL.validation_fail!(:PLMC27, "Formats supplied to aggregation functions must have a :format key", :error) unless format.keys.include?(:format) + DDL.validation_fail!(:PLMC26, "Functions supplied to aggregate should be a hash", :error) unless function.is_a?(Hash) + + unless (function.keys.include?(:args)) && function[:args] + DDL.validation_fail!(:PLMC25, "aggregate method for action '%{action}' missing a function parameter", :error, :action => entities[@current_entity][:action]) + end + + entities[@current_entity][:aggregate] ||= [] + entities[@current_entity][:aggregate] << (format[:format].nil? ? function : function.merge(format)) + end + + # Sets the display preference to either :ok, :failed, :flatten or :always + # operates on action level + def display(pref) + # defaults to old behavior, complain if its supplied and invalid + unless [:ok, :failed, :flatten, :always].include?(pref) + raise "Display preference #{pref} is not valid, should be :ok, :failed, :flatten or :always" + end + + action = @current_entity + @entities[action][:display] = pref + end + + # Creates the definition for an action, you can nest input definitions inside the + # action to attach inputs and validation to the actions + # + # action "status", :description => "Restarts a Service" do + # display :always + # + # input "service", + # :prompt => "Service Action", + # :description => "The action to perform", + # :type => :list, + # :optional => true, + # :list => ["start", "stop", "restart", "status"] + # + # output "status", + # :description => "The status of the service after the action" + # + # end + def action(name, input, &block) + raise "Action needs a :description property" unless input.include?(:description) + + unless @entities.include?(name) + @entities[name] = {} + @entities[name][:action] = name + @entities[name][:input] = {} + @entities[name][:output] = {} + @entities[name][:display] = :failed + @entities[name][:description] = input[:description] + end + + # if a block is passed it might be creating input methods, call it + # we set @current_entity so the input block can know what its talking + # to, this is probably an epic hack, need to improve. + @current_entity = name + block.call if block_given? + @current_entity = nil + end + + # If the method name matches a # aggregate function, we return the function + # with args as a hash. This will only be active if the @process_aggregate_functions + # is set to true which only happens in the #summarize block + def method_missing(name, *args, &block) + unless @process_aggregate_functions || is_function?(name) + raise NoMethodError, "undefined local variable or method `#{name}'", caller + end + + return {:function => name, :args => args} + end + + # Checks if a method name matches a aggregate plugin. + # This is used by method missing so that we dont greedily assume that + # every method_missing call in an agent ddl has hit a aggregate function. + def is_function?(method_name) + PluginManager.find("aggregate").include?(method_name.to_s) + end + + # For a given action and arguments look up the DDL interface to that action + # and if any arguments in the DDL have a :default value assign that to any + # input that does not have an argument in the input arguments + # + # This is intended to only be called on clients and not on servers as the + # clients should never be able to publish non compliant requests and the + # servers should really not tamper with incoming requests since doing so + # might raise validation errors that were not raised on the client breaking + # our fail-fast approach to input validation + def set_default_input_arguments(action, arguments) + input = action_interface(action)[:input] + + return unless input + + input.keys.each do |key| + if !arguments.include?(key) && !input[key][:default].nil? && !input[key][:optional] + Log.debug("Setting default value for input '%s' to '%s'" % [key, input[key][:default]]) + arguments[key] = input[key][:default] + end + end + end + + # Helper to use the DDL to figure out if the remote call to an + # agent should be allowed based on action name and inputs. + def validate_rpc_request(action, arguments) + # is the action known? + unless actions.include?(action) + DDL.validation_fail!(:PLMC29, "Attempted to call action %{action} for %{plugin} but it's not declared in the DDL", :debug, :action => action, :plugin => @pluginname) + end + + input = action_interface(action)[:input] || {} + + input.keys.each do |key| + unless input[key][:optional] + unless arguments.keys.include?(key) + DDL.validation_fail!(:PLMC30, "Action '%{action}' needs a '%{key}' argument", :debug, :action => action, :key => key) + end + end + + if arguments.keys.include?(key) + validate_input_argument(input, key, arguments[key]) + end + end + + true + end + + # Returns the interface for a specific action + def action_interface(name) + @entities[name] || {} + end + + # Returns an array of actions this agent support + def actions + @entities.keys + end + end + end +end diff --git a/lib/mcollective/ddl/base.rb b/lib/mcollective/ddl/base.rb new file mode 100644 index 0000000..d526755 --- /dev/null +++ b/lib/mcollective/ddl/base.rb @@ -0,0 +1,223 @@ +module MCollective + module DDL + # The base class for all kinds of DDL files. DDL files when + # run gets parsed and builds up a hash of the basic primitive + # types, ideally restricted so it can be converted to JSON though + # today there are some Ruby Symbols in them which might be fixed + # laster on. + # + # The Hash being built should be stored in @entities, the format + # is generally not prescribed but there's a definite feel to how + # DDL files look so study the agent and discovery ones to see how + # the structure applies to very different use cases. + # + # For every plugin type you should have a single word name - that + # corresponds to the directory in the libdir where these plugins + # live. If you need anything above and beyond 'metadata' in your + # plugin DDL then add a PlugintypeDDL class here and add your + # specific behaviors to those. + class Base + include Translatable + + attr_reader :meta, :entities, :pluginname, :plugintype, :usage, :requirements + + def initialize(plugin, plugintype=:agent, loadddl=true) + @entities = {} + @meta = {} + @usage = "" + @config = Config.instance + @pluginname = plugin + @plugintype = plugintype.to_sym + @requirements = {} + + loadddlfile if loadddl + end + + # Generates help using the template based on the data + # created with metadata and input. + # + # If no template name is provided one will be chosen based + # on the plugin type. If the provided template path is + # not absolute then the template will be loaded relative to + # helptemplatedir configuration parameter + def help(template=nil) + template = template_for_plugintype unless template + template = File.join(@config.helptemplatedir, template) unless template.start_with?(File::SEPARATOR) + + template = File.read(template) + meta = @meta + entities = @entities + + unless template == "metadata-help.erb" + metadata_template = File.join(@config.helptemplatedir, "metadata-help.erb") + metadata_template = File.read(metadata_template) + metastring = ERB.new(metadata_template, 0, '%') + metastring = metastring.result(binding) + end + + erb = ERB.new(template, 0, '%') + erb.result(binding) + end + + def usage(usage_text) + @usage = usage_text + end + + def template_for_plugintype + case @plugintype + when :agent + return "rpc-help.erb" + else + if File.exists?(File.join(@config.helptemplatedir,"#{@plugintype}-help.erb")) + return "#{@plugintype}-help.erb" + else + # Default help template gets loaded if plugintype-help does not exist. + return "metadata-help.erb" + end + end + end + + def loadddlfile + if ddlfile = findddlfile + instance_eval(File.read(ddlfile), ddlfile, 1) + else + raise_code(:PLMC18, "Can't find DDL for %{type} plugin '%{name}'", :debug, :type => @plugintype, :name => @pluginname) + end + end + + def findddlfile(ddlname=nil, ddltype=nil) + ddlname = @pluginname unless ddlname + ddltype = @plugintype unless ddltype + + @config.libdir.each do |libdir| + ddlfile = File.join([libdir, "mcollective", ddltype.to_s, "#{ddlname}.ddl"]) + if File.exist?(ddlfile) + log_code(:PLMC18, "Found %{ddlname} ddl at %{ddlfile}", :debug, :ddlname => ddlname, :ddlfile => ddlfile) + return ddlfile + end + end + return false + end + + def validate_requirements + if requirement = @requirements[:mcollective] + if Util.mcollective_version == "@DEVELOPMENT_VERSION@" + log_code(:PLMC19, "DDL requirements validation being skipped in development", :warn) + return true + end + + if Util.versioncmp(Util.mcollective_version, requirement) < 0 + DDL.validation_fail!(:PLMC20, "%{type} plugin '%{name}' requires MCollective version %{requirement} or newer", :debug, :type => @plugintype.to_s.capitalize, :name => @pluginname, :requirement => requirement) + end + end + + true + end + + # validate strings, lists and booleans, we'll add more types of validators when + # all the use cases are clear + # + # only does validation for arguments actually given, since some might + # be optional. We validate the presense of the argument earlier so + # this is a safe assumption, just to skip them. + # + # :string can have maxlength and regex. A maxlength of 0 will bypasss checks + # :list has a array of valid values + def validate_input_argument(input, key, argument) + Validator.load_validators + + case input[key][:type] + when :string + Validator.validate(argument, :string) + + Validator.length(argument, input[key][:maxlength].to_i) + + Validator.validate(argument, input[key][:validation]) + + when :list + Validator.validate(argument, input[key][:list]) + + else + Validator.validate(argument, input[key][:type]) + end + + return true + rescue => e + DDL.validation_fail!(:PLMC21, "Cannot validate input '%{input}': %{error}", :debug, :input => key, :error => e.to_s) + end + + # Registers an input argument for a given action + # + # See the documentation for action for how to use this + def input(argument, properties) + raise_code(:PLMC22, "Cannot determine what entity input '%{entity}' belongs to", :error, :entity => @current_entity) unless @current_entity + + entity = @current_entity + + [:prompt, :description, :type].each do |arg| + raise_code(:PLMC23, "Input needs a :%{property} property", :debug, :property => arg) unless properties.include?(arg) + end + + @entities[entity][:input][argument] = {:prompt => properties[:prompt], + :description => properties[:description], + :type => properties[:type], + :default => properties[:default], + :optional => properties[:optional]} + + case properties[:type] + when :string + raise "Input type :string needs a :validation argument" unless properties.include?(:validation) + raise "Input type :string needs a :maxlength argument" unless properties.include?(:maxlength) + + @entities[entity][:input][argument][:validation] = properties[:validation] + @entities[entity][:input][argument][:maxlength] = properties[:maxlength] + + when :list + raise "Input type :list needs a :list argument" unless properties.include?(:list) + + @entities[entity][:input][argument][:list] = properties[:list] + end + end + + # Registers an output argument for a given action + # + # See the documentation for action for how to use this + def output(argument, properties) + raise "Cannot figure out what action input #{argument} belongs to" unless @current_entity + raise "Output #{argument} needs a description argument" unless properties.include?(:description) + raise "Output #{argument} needs a display_as argument" unless properties.include?(:display_as) + + action = @current_entity + + @entities[action][:output][argument] = {:description => properties[:description], + :display_as => properties[:display_as], + :default => properties[:default]} + end + + def requires(requirement) + raise "Requirement should be a hash in the form :item => 'requirement'" unless requirement.is_a?(Hash) + + valid_requirements = [:mcollective] + + requirement.keys.each do |key| + unless valid_requirements.include?(key) + raise "Requirement %s is not a valid requirement, only %s is supported" % [key, valid_requirements.join(", ")] + end + + @requirements[key] = requirement[key] + end + + validate_requirements + end + + # Registers meta data for the introspection hash + def metadata(meta) + [:name, :description, :author, :license, :version, :url, :timeout].each do |arg| + raise "Metadata needs a :#{arg} property" unless meta.include?(arg) + end + + @meta = meta + end + end + end +end diff --git a/lib/mcollective/ddl/dataddl.rb b/lib/mcollective/ddl/dataddl.rb new file mode 100644 index 0000000..1f7cdcd --- /dev/null +++ b/lib/mcollective/ddl/dataddl.rb @@ -0,0 +1,56 @@ +module MCollective + module DDL + # A DDL file for the data query plugins. + # + # Query plugins can today take only one input by convention in the DDL that + # is called :query, otherwise the input is identical to the standard input. + # + # metadata :name => "Agent", + # :description => "Meta data about installed MColletive Agents", + # :author => "R.I.Pienaar ", + # :license => "ASL 2.0", + # :version => "1.0", + # :url => "http://marionette-collective.org/", + # :timeout => 1 + # + # dataquery :description => "Agent Meta Data" do + # input :query, + # :prompt => "Agent Name", + # :description => "Valid agent name", + # :type => :string, + # :validation => /^[\w\_]+$/, + # :maxlength => 20 + # + # [:license, :timeout, :description, :url, :version, :author].each do |item| + # output item, + # :description => "Agent #{item}", + # :display_as => item.to_s.capitalize + # end + # end + class DataDDL input[:description], + :input => {}, + :output => {}} + + @current_entity = :data + block.call if block_given? + @current_entity = nil + end + + def input(argument, properties) + raise "The only valid input name for a data query is 'query'" if argument != :query + + super + end + + # Returns the interface for the data query + def dataquery_interface + @entities[:data] || {} + end + end + end +end diff --git a/lib/mcollective/ddl/discoveryddl.rb b/lib/mcollective/ddl/discoveryddl.rb new file mode 100644 index 0000000..041865c --- /dev/null +++ b/lib/mcollective/ddl/discoveryddl.rb @@ -0,0 +1,52 @@ +module MCollective + module DDL + # DDL for discovery plugins, a full example can be seen below + # + # metadata :name => "mc", + # :description => "MCollective Broadcast based discovery", + # :author => "R.I.Pienaar ", + # :license => "ASL 2.0", + # :version => "0.1", + # :url => "http://marionette-collective.org/", + # :timeout => 2 + # + # discovery do + # capabilities [:classes, :facts, :identity, :agents, :compound] + # end + class DiscoveryDDL []} + + @current_entity = :discovery + block.call if block_given? + @current_entity = nil + end + end + end +end diff --git a/lib/mcollective/ddl/validatorddl.rb b/lib/mcollective/ddl/validatorddl.rb new file mode 100644 index 0000000..8c7093b --- /dev/null +++ b/lib/mcollective/ddl/validatorddl.rb @@ -0,0 +1,6 @@ +module MCollective + module DDL + class ValidatorDDL 0 + return discovered[0,limit] + else + return discovered + end + end + end +end diff --git a/lib/mcollective/exception.rb b/lib/mcollective/exception.rb new file mode 100644 index 0000000..a6cade5 --- /dev/null +++ b/lib/mcollective/exception.rb @@ -0,0 +1,40 @@ +module MCollective + class CodedError default}.merge(@args)) + + super(msg) + end + + def set_backtrace(trace) + super + log(@log_level) + end + + def log(level, log_backtrace=false) + Log.logexception(@code, level, self, log_backtrace) + end + end + + # Exceptions for the RPC system + class DDLValidationError "bar", + # "bar" => "baz"} + class Base + def initialize + @facts = {} + @last_good_facts = {} + @last_facts_load = 0 + end + + # Registers new fact sources into the plugin manager + def self.inherited(klass) + PluginManager << {:type => "facts_plugin", :class => klass.to_s} + end + + # Returns the value of a single fact + def get_fact(fact=nil) + config = Config.instance + + cache_time = config.fact_cache_time || 300 + + Thread.exclusive do + begin + if (Time.now.to_i - @last_facts_load > cache_time.to_i ) || force_reload? + Log.debug("Resetting facter cache, now: #{Time.now.to_i} last-known-good: #{@last_facts_load}") + + tfacts = load_facts_from_source + + # Force reset to last known good state on empty facts + raise "Got empty facts" if tfacts.empty? + + @facts.clear + + tfacts.each_pair do |key,value| + @facts[key.to_s] = value.to_s + end + + @last_good_facts = @facts.clone + @last_facts_load = Time.now.to_i + else + Log.debug("Using cached facts now: #{Time.now.to_i} last-known-good: #{@last_facts_load}") + end + rescue Exception => e + Log.error("Failed to load facts: #{e.class}: #{e}") + + # Avoid loops where failing fact loads cause huge CPU + # loops, this way it only retries once every cache_time + # seconds + @last_facts_load = Time.now.to_i + + # Revert to last known good state + @facts = @last_good_facts.clone + end + end + + + # If you do not supply a specific fact all facts will be returned + if fact.nil? + return @facts + else + @facts.include?(fact) ? @facts[fact] : nil + end + end + + # Returns all facts + def get_facts + get_fact(nil) + end + + # Returns true if we know about a specific fact, false otherwise + def has_fact?(fact) + get_fact(nil).include?(fact) + end + + # Plugins can override this to provide forced fact invalidation + def force_reload? + false + end + end + end +end diff --git a/lib/mcollective/generators.rb b/lib/mcollective/generators.rb new file mode 100644 index 0000000..d721e4e --- /dev/null +++ b/lib/mcollective/generators.rb @@ -0,0 +1,7 @@ +module MCollective + module Generators + autoload :Base, "mcollective/generators/base.rb" + autoload :DataGenerator, "mcollective/generators/data_generator.rb" + autoload :AgentGenerator, "mcollective/generators/agent_generator.rb" + end +end diff --git a/lib/mcollective/generators/agent_generator.rb b/lib/mcollective/generators/agent_generator.rb new file mode 100644 index 0000000..577125e --- /dev/null +++ b/lib/mcollective/generators/agent_generator.rb @@ -0,0 +1,51 @@ +module MCollective + module Generators + class AgentGenerator \"%ACTIONDESCRIPTION%\" do\n" + action_text += action_help if i == 0 + action_text += "end\n" + action_text += "\n" unless @actions.size == (i + 1) + end + # Use inherited method to create metadata part of the ddl + create_metadata_string + action_text + end + + def create_plugin_content + content_text = "" + + # Add actions to agent file + @actions.each_with_index do |action, i| + content_text += "%6s%s" % [" ", "action \"#{action}\" do\n"] + content_text += "%6s%s" % [" ", "end\n"] + content_text += "\n" unless @actions.size == (i + 1) + end + content_text + end + + def action_help + action_snippet = File.read(File.join(File.dirname(__FILE__), "templates", "action_snippet.erb")) + ERB.new(action_snippet).result + end + end + end +end diff --git a/lib/mcollective/generators/base.rb b/lib/mcollective/generators/base.rb new file mode 100644 index 0000000..57ff3a0 --- /dev/null +++ b/lib/mcollective/generators/base.rb @@ -0,0 +1,46 @@ +module MCollective + module Generators + class Base + attr_accessor :meta, :plugin_name, :mod_name + def initialize(name, description, author, license, version, url, timeout) + @meta = {:name => name, + :description => description, + :author => author, + :license => license, + :version => version, + :url => url, + :timeout => timeout} + end + + def create_metadata_string + ddl_template = File.read(File.join(File.dirname(__FILE__), "templates", "ddl.erb")) + ERB.new(ddl_template, nil, "-").result(binding) + end + + def create_plugin_string + plugin_template = File.read(File.join(File.dirname(__FILE__), "templates", "plugin.erb")) + ERB.new(plugin_template, nil, "-").result(binding) + end + + def write_plugins + begin + Dir.mkdir @plugin_name + dirname = File.join(@plugin_name, @mod_name.downcase) + Dir.mkdir dirname + puts "Created plugin directory : #{@plugin_name}" + + File.open(File.join(dirname, "#{@plugin_name}.ddl"), "w"){|f| f.puts @ddl} + puts "Created DDL file : #{File.join(dirname, "#{@plugin_name}.ddl")}" + + File.open(File.join(dirname, "#{@plugin_name}.rb"), "w"){|f| f.puts @plugin} + puts "Created #{@mod_name} file : #{File.join(dirname, "#{@plugin_name}.rb")}" + rescue Errno::EEXIST + raise "cannot generate '#{@plugin_name}' : plugin directory already exists." + rescue Exception => e + FileUtils.rm_rf(@plugin_name) if File.directory?(@plugin_name) + raise "cannot generate plugin - #{e}" + end + end + end + end +end diff --git a/lib/mcollective/generators/data_generator.rb b/lib/mcollective/generators/data_generator.rb new file mode 100644 index 0000000..a7d7285 --- /dev/null +++ b/lib/mcollective/generators/data_generator.rb @@ -0,0 +1,51 @@ +module MCollective + module Generators + class DataGenerator \"%DESCRIPTION%\",\n"] + query_text += "%9s%s" % [" ", ":display_as => \"%DESCRIPTION%\"\n"] + query_text += "\n" unless @outputs.size == (i + 1) + end + + query_text += "end" + + # Use inherited method to create metadata part of the ddl + create_metadata_string + query_text + end + + def create_plugin_content + content_text = "%6s%s" % [" ", "query do |what|\n"] + + @outputs.each do |output| + content_text += "%8s%s" % [" ", "result[:#{output}] = nil\n"] + end + content_text += "%6s%s" % [" ", "end\n"] + + # Add actions to agent file + content_text + end + end + end +end diff --git a/lib/mcollective/generators/templates/action_snippet.erb b/lib/mcollective/generators/templates/action_snippet.erb new file mode 100644 index 0000000..9b43f68 --- /dev/null +++ b/lib/mcollective/generators/templates/action_snippet.erb @@ -0,0 +1,13 @@ + # Example Input + input :name, + :prompt => "%PROMPT%", + :description => "%DESCRIPTION%", + :type => %TYPE%, + :validation => '%VALIDATION%', + :optional => %OPTIONAL%, + :maxlength => %MAXLENGTH% + + # Example output + output :name, + :description => "%DESCRIPTION%", + :display_as => "%DISPLAYAS%" diff --git a/lib/mcollective/generators/templates/data_input_snippet.erb b/lib/mcollective/generators/templates/data_input_snippet.erb new file mode 100644 index 0000000..7a3fab9 --- /dev/null +++ b/lib/mcollective/generators/templates/data_input_snippet.erb @@ -0,0 +1,7 @@ + input :query, + :prompt => "%PROMP%", + :description => "%DESCRIPTION%", + :type => %TYPE%, + :validation => %VALIDATION%, + :maxlength => %MAXLENGTH% + diff --git a/lib/mcollective/generators/templates/ddl.erb b/lib/mcollective/generators/templates/ddl.erb new file mode 100644 index 0000000..2f82ef0 --- /dev/null +++ b/lib/mcollective/generators/templates/ddl.erb @@ -0,0 +1,8 @@ +metadata :name => "<%= meta[:name] || "%FULLNANE%" -%>", + :description => "<%= meta[:description] || "%DESCRIPTION%" -%>", + :author => "<%= meta[:author] || "%AUTHOR%" -%>", + :license => "<%= meta[:license] || "%LICENSE%" -%>", + :version => "<%= meta[:version] || "%VERSION%" -%>", + :url => "<%= meta[:url] || "%URL%" -%>", + :timeout => <%= meta[:timeout] || "%TIMEOUT%" %> + diff --git a/lib/mcollective/generators/templates/plugin.erb b/lib/mcollective/generators/templates/plugin.erb new file mode 100644 index 0000000..db7e9d1 --- /dev/null +++ b/lib/mcollective/generators/templates/plugin.erb @@ -0,0 +1,7 @@ +module MCollective + module <%= @mod_name%> + class <%= @plugin_name.capitalize -%><<%= @pclass%> +<%= @content%> + end + end +end diff --git a/lib/mcollective/locales/en.yml b/lib/mcollective/locales/en.yml new file mode 100644 index 0000000..2ff5d7a --- /dev/null +++ b/lib/mcollective/locales/en.yml @@ -0,0 +1,321 @@ +en: + PLMC1: + example: "The Marionette Collective version 2.2.2 started by /usr/bin/mco using config file /etc/mcollective/client.cfg" + expanded: "This message gets logged each time MCollective reads it's config file. Generally this only happens once per process. It shows the version, process name and config file as a simple debugging aid" + pattern: "The Marionette Collective version %{version} started by %{name} using config file %{config}" + PLMC10: + example: "Failed to handle message: RuntimeError: none.rb:15:in `decodemsg': Could not decrypt message " + expanded: |- + When a message arrives from the middleware it gets decoded, security validated and then dispatched to the agent code. + + There exist a number of errors that can happen here, some are handled specifically others will be logged by this "catch all" handler. + + Generally there should not be many messages logged here but we include a stack trace to assist with debugging these. + + The messages here do not tend to originate from your Agents unless they are syntax error related but more likely to be situations like security failures due to incorrect SSL keys and so forth + + Should you come across one that is a regular accorance in your logs please open a ticket including your backtrace and we will improve the handling of that situation + pattern: "Failed to handle message: %{error}" + PLMC11: + example: "Cache expired on 'ddl' key 'agent/nrpe'" + expanded: |- + MCollective has an internal Cache used to speed up operations like parsing of DDL files. The cache is also usable from the agents and other plugins you write. + + Each entry in the cache has an associated TTL or maximum life time, once the maximum time on an item is reached it is considered expired. Next time anything attempts to read this entry from the cache this log line will be logged. + + This is part of the normal operations of MCollective and doesn't indicate any problem. We log this debug message to help you debug your own use of the cache. + pattern: "Cache expired on '%{cache_name}' key '%{item}'" + PLMC12: + example: "Cache hit on 'ddl' key 'agent/nrpe'" + expanded: |- + MCollective has an internal Cache used to speed up operations like parsing of DDL files. The cache is also usable from the agents and other plugins you write. + + Each entry in the cache has an associated TTL or maximum life time, once the maximum time on an item is reached it is considered expired. + + This log line indicates that a request for a cache entry was made, the entry had not yet expired and so the cached data is being returned. + + It does not indicate a problem, it's just a debugging aid + pattern: "Cache hit on '%{cache_name}' key '%{item}'" + PLMC13: + example: "Could not find a cache called 'my_cache'" + expanded: |- + MCollective has an internal Cache used to speed up operations like parsing of DDL files. The cache is also usable from the agents and other plugins you write. + + The cache is made up of many named caches, this error indicates that a cache has not yet been setup prior to trying to use it. + pattern: "Could not find a cache called '%{cache_name}'" + PLMC14: + example: "No block supplied to synchronize on cache 'my_cache'" + expanded: |- + When using the Cache to synchronize your own code across agents or other plugins you have to supply a block to synchronise. + + Correct usage would be: + + Cache.setup(:customer, 600) + Cache(:customer).synchronize do + # update customer record + end + + This error is raise when the logic to update the customer record is not in a block as in the example + pattern: "No block supplied to synchronize on cache '%{cache_name}'" + PLMC15: + example: "No item called 'nrpe_agent' for cache 'ddl'" + expanded: |- + MCollective has an internal Cache used to speed up operations like parsing of DDL files. The cache is also usable from the agents and other plugins you write. + + The cache stored items using a key, this error will be logged and raised when you try to access a item that does not yet exist in the cache. + pattern: "No item called '%{item}' for cache '%{cache_name}'" + PLMC16: + example: "'hello' does not look like a numeric value" + expanded: |- + When MCollective receives an argument from the command line like port=fello it consults the DDL file to determine the desired type of this parameter, it then tries to convert the input string into the correct numeric value. + + This error indicates the input you provided could not be converted into the desired format. + + You'll usually see this when using the 'mco rpc' command to interact with an agent. This is usually a fatal error, the request will not be sent if it does not validate against the DDL. + pattern: "'%{value}' does not look like a numeric value" + PLMC17: + example: "'flase' does not look like a boolean value" + expanded: |- + When MCollective receives an argument from the command line like force=true it consults the DDL file to determine the desired type of this parameter, it then tries to convert the input string into the correct boolean value. + + This error indicates the input you provided could not be converted into the desired boolean format. It wil accept "true", "t", "yes", "y" and "1" as being true. It will accept "false", "f", "no", "n", "0" as being false. + + You'll usually see this when using the 'mco rpc' command to interact with an agent. This is usually a fatal error, the request will not be sent if it does not validate against the DDL. + pattern: "'%{value}' does not look like a boolean value" + PLMC18: + example: "Found 'rpcutil' ddl at '/usr/libexec/mcollective/mcollective/agent/rpcutil.ddl'" + expanded: |- + Most plugin types have a DDL file that needs to be correctly installed for the plugin to function. There can be multiple libdirs that can provide the DDL file for a specific plugin. + + This message is a message designed to help you debug issues when you think the wrong DDL file is being used. + pattern: "Found '%{ddlname}' ddl at '%{ddlfile}'" + PLMC19: + expanded: |- + Usually when MCollective run it validates all requirements are met before publishing a request or processing a request against the DDL file for the agent. + + This can be difficult to satisfy in development perhaps because you are still writing your DDL files or debugging issues. + + This message indicates that when MCollective detects it's running against an unreleased version of MCollective - like directly out of a Git clone - it will skip the DDL validation steps. It is logged at a warning level as this significantly changes the behaviour of the client and server. + pattern: "DDL requirements validation being skipped in development" + PLMC2: + expanded: "When sending the mcollectived process the USR1 signal on a Unix based machine this message will indicate that the signal got received and all agents are being reloaded from disk\n" + pattern: "Reloading all agents after receiving USR1 signal" + PLMC20: + example: "Agent plugin 'example' requires MCollective version 2.2.1 or newer" + expanded: |- + MCollective plugins can declare a minimum version of MCollective that they require to be functional. + + MCollective validates this when it loads the plugin and this error will be logged or shown to the user when this requirement cannot be satisfied. + pattern: "%{type} plugin '%{name}' requires MCollective version %{requirement} or newer" + PLMC21: + example: "Cannot validate input 'service': Input string is longer than 40 character(s)" + expanded: |- + Every input you provide to a RPC request is validated against it's DDL file. This error will be shown when the supplied data does not pass validation against the DDL. + + Consult the 'mco plugin doc' command to view the DDL file for your action and input. + pattern: "Cannot validate input '%{input}: %{error}" + PLMC22: + example: "Cannot determine what entity input 'port' belongs to" + expanded: |- + When writing a DDL you declare inputs into plugins using the input keyword. Each input has to belong to a wrapping entity like in the example below: + + action "get_data", :description => "Get data from a data plugin" do + input :source, + :prompt => "Data Source", + :description => "The data plugin to retrieve information from", + :type => :string, + :validation => '^\w+$', + :optional => false, + :maxlength => 50 + end + + Here the input belongs to the action entity "get_data", this error indicates that an input were found without it belonging to any specific entity + pattern: "Cannot determine what entity input '%{input}' belongs to" + PLMC23: + example: "Input needs a :prompt property" + expanded: "When writing a DDL you declare inputs for all data that you pass into the plugin. Inputs have a minimally required field set and this error indicate that you did not provide some key field while describing the input." + pattern: "Input needs a :%{property} property" + PLMC24: + example: "Failed to load DDL for the 'rpcutil' agent, DDLs are required: RuntimeError: failed to parse DDL file" + expanded: |- + As of version 2.0.0 DDL files are required by the MCollective server. This server indicates that it either could not find the DDL for the rpcutil agent or that there was a syntax error. + + Confirm that the DDL file is installed in the agent directory and that it parses correctly using the 'mco plugin doc' command. + pattern: "Failed to load DDL for the '%{agent}' agent, DDLs are required: %{error_class}: %{error}" + PLMC25: + example: "aggregate method for action 'rpcutil' is missing a function parameter" + expanded: |- + DDL files can declare aggregation rules for the data returned by actions as seen below: + + summarize do + aggregate summary(:collectives) + end + + This error indicates that you did not supply the argument - :collectives in this example. + pattern: "aggregate method for action '%{action}' is missing a function parameter" + PLMC26: + expanded: |- + Internally when MCollective parse the DDL it converts the aggregate functions into a hash with the function name and any arguments. + + This error indicates that the internal translation failed, this is a critical error and would probably indicate a structure problem in your DDL or a bug in MCollective + pattern: "Functions supplied to aggregate should be a hash" + PLMC27: + expanded: |- + DDL aggregate functions can take a custom format as in the example below: + + aggregate average(:total_resources, :format => "Average: %d") + + This error indicate that the :format above could not be correctly parsed. + pattern: "Formats supplied to aggregation functions must have a :format key" + PLMC28: + expanded: |- + DDL aggregate functions can take a custom format as in the example below: + + aggregate average(:total_resources, :format => "Average: %d") + + This error indicate that the :format above was not supplied as a hash as in the above example + pattern: "Formats supplied to aggregation functions should be a hash" + PLMC29: + example: "Attempted to call action 'yum_clean' for 'package' but it's not declared in the DDL" + expanded: |- + Every agent has a DDL file that describes valid actions that they can perform + + This error indicates you attempted to perform an action that does not exist. Review the plugin documentation using 'mco plugin doc' for correct usage + pattern: "Attempted to call action '%{action}' for '%{plugin}' but it's not declared in the DDL" + PLMC3: + expanded: |- + When sending the mcollectived process the USR2 signal on a Unix based machine this message indicates that the signal for received and the logging level is being adjusted to the next higher level or back down to debug if it was already at it's highest level. + + This message will be followed by another message similar to the one below: + + Logging level is now WARN + pattern: "Cycling logging level due to USR2 signal" + PLMC30: + example: "Action 'get_fact' needs a 'fact' argument" + expanded: "Actions can declare that they expect a required input, this error indicates that you did not supply the required input" + pattern: "Action '%{action}' needs a '%{key}' argument" + PLMC31: + example: "No dataquery has been defined in the DDL for data plugin 'package'" + expanded: |- + Each data plugin requires a DDL that has a 'dataquery' block in it. + + dataquery :description => "Agent Meta Data" do + . + . + end + + This error indicates that the DDL file for a specific data plugin did not contain this block. + pattern: "No dataquery has been defined in the DDL for data plugin '%{plugin}'" + PLMC32: + expanded: "Data plugins must return data. The DDL files for a data plugin must declare at least one output parameter else you will see this error." + pattern: "No output has been defined in the DDL for data plugin '%{plugin}'" + PLMC33: + example: "No data plugin argument was declared in the 'puppet' DDL but an input was supplied" + expanded: |- + Data plugins can take an optional input argument. This error indicate that you supplied an input argument but the plugin does not actually expect any input. + + Review the documentation for the data plugin using 'mco plugin doc' + pattern: "No data plugin argument was declared in the '%{plugin}' DDL but an input was supplied" + PLMC34: + example: "setting meta data in agents have been deprecated, DDL files are now being used for this information. Please update the 'package' agent" + expanded: |- + In the past each MCollective agent had a metadata section containing information like the timeout. + + As of 2.2.0 the agents will now consult the DDL files that ship with each agent for this purpose so the metadata in agents are not used at all. + + In order to remove confusing duplication setting metadata in agents have been deprecated and from version 2.4.0 will not be supported at all. + + Please update your agent by removing the metadata section from it and make sure the DDL file has the correct information instead. + pattern: "setting meta data in agents have been deprecated, DDL files are now being used for this information. Please update the '%{agent}' agent" + PLMC35: + expanded: |- + The MCollective client can ask that the agent just performs the action and never respond. Like when supplying the --no-results option to the 'mco rpc' application. + + This log line indicates that the request was received and interpreted as such and no reply will be sent. This does not indicate a problem generally it's just there to assist with debugging of problems. + pattern: "Client did not request a response, surpressing reply" + PLMC36: + example: "Unknown action 'png' for agent 'rpcutil'" + expanded: |- + Agents are made up of a number of named actions. When the MCollective Server receives a request it double checks if the agent in question actually implements the logic for a specific action. + + When it cannot find the implementation this error will be raised. This is an unusual situation since at this point on both the client and the server the DDL will already have been used to validate the request and the DDL would have indicated that the action is valid. + + Check your agent code and make sure you have code like: + + action "the_action" do + . + . + end + pattern: "Unknown action '%{action}' for agent '%{agent}'" + PLMC37: + example: "Starting default activation checks for the 'rpcutil' agent" + expanded: |- + Each time the MCollective daemon starts it loads each agent from disk. It then tries to determine if the agent should start on this node by using the activate_when method or per-agent configuration. + + This is a debug statement that shows you it is about to start interacting with the logic in the agent to determine if it should be made available or not. + pattern: "Starting default activation checks for the '%{agent}' agent" + PLMC38: + example: "Found plugin configuration 'exim.activate_agent' with value 'y'" + expanded: |- + The administrator can choose that a certain agent that is deployed on this machine should not be made available to the network. + + To do this you would add a configuration value like this example to the mcollective server.cfg: + + plugin.exim.activate_agent = n + + If this value is "1", "y" or "true" the agent will be activated else it will be disabled. + pattern: "Found plugin configuration '%{agent}.activate_agent' with value '%{should_activate}'" + PLMC39: + example: "Audit failed with an error, processing the request will continue." + expanded: |- + Every MCollective request can be audited. In the event where the auditing fails the processing continues. + + At present the failure handling is not configurable, in future the administrator might elect to not run unaudited requests via configuration. + PLMC4: + example: "Failed to start registration plugin: ArgumentError: meta.rb:6:in `gsub': wrong number of arguments (0 for 2)" + expanded: | + Registration plugins are loaded into the MCollective daemon at startup and ran on a regular interval. + + This message indicate that on first start this plugin failed to run, it will show the exception class, line and exception message to assist with debugging + pattern: "Failed to start registration plugin: %{error}" + PLMC5: + expanded: |- + In previous versions of MCollective a application called 'controller' were included that had the ability to request agent reloads and other commands that would control the runner. + + Unfortunately this method of controlling the daemon was never considered stable or reliable and has since been deprecate for removal during the 2.3.x development series + pattern: "Received a control message, possibly via 'mco controller' but this has been deprecated" + PLMC6: + expanded: |- + When a specific MCollective daemon receives a message from a network it validates the filter before processing the message. The filter is the list of classes, facts or other conditions that are associated with any message. + + In the case where the filter does not match the specific host this line gets logged. + + It's often the case for broadcast messages that all MCollective nodes will receive a message but only a subset of nodes are targetted using the filters, in that situation the nodes that received the request but should not respond to it will see this log line. + + It does not indicate anything to be concerned about but if you find debugging a problem and expect a node to have responded when it shouldn't this log line will give you a hint that some condition specified in the filter did not match the local host + pattern: "Message does not pass filters, ignoring" + PLMC7: + example: "Exiting after signal: SignalException: runner.rb:6:in `run': Interrupt" + expanded: |- + When the MCollective daemon gets a signal from the Operating System that it does not specifically handle it will log this line before exiting. + + You would see this whenever the daemon is stopped by init script or when sending it a kill signal, it will then proceed to disconnect from the middleware and exit its main loop + pattern: "Exiting after signal: %{error}" + PLMC8: + example: "Handling message for agent 'rpcutil' on collective 'eu_collective' with requestid 'a8a78d0ff555c931f045b6f448129846'" + expanded: |- + After receiving a message from the middleware, decoding it, validating it's security credentials and doing other checks on it the MCollective daemon will pass it off to the actual agent code for processing. + + Prior to doing so it will log this line indicating the agent name and sub-collective and other information that will assist in correlating the message sent from the client with those in the server logs being processed. + pattern: "Handling message for agent '%{agent}' on collective '%{collective} with requestid '%{requestid}'" + PLMC9: + example: "Expired Message: message 8b4fe522f0d0541dabe83ec10b7fa446 from cert=client@node created at 1358840888 is 653 seconds old, TTL is 60" + expanded: |- + Requests sent from clients to servers all have a creation time and a maximum validity time called a TTL. + + This message indicates that a message was received from the network but that it was determined to be too based on the TTL settings. + + Usually this happens because your clocks are not in sync - something that can be fixed by rolling out a tool like ntp across your server estate. + + It might also happen during very slow network conditions or when the TTL is set too low for your general network latency. + pattern: "Expired Message: %{error}" diff --git a/lib/mcollective/log.rb b/lib/mcollective/log.rb new file mode 100644 index 0000000..7e3d774 --- /dev/null +++ b/lib/mcollective/log.rb @@ -0,0 +1,168 @@ +module MCollective + # A simple class that allows logging at various levels. + class Log + class << self + @logger = nil + + VALID_LEVELS = [:error, :fatal, :debug, :warn, :info] + + # Obtain the class name of the currently configured logger + def logger + @logger.class + end + + # Logs at info level + def info(msg) + log(:info, msg) + end + + # Logs at warn level + def warn(msg) + log(:warn, msg) + end + + # Logs at debug level + def debug(msg) + log(:debug, msg) + end + + # Logs at fatal level + def fatal(msg) + log(:fatal, msg) + end + + # Logs at error level + def error(msg) + log(:error, msg) + end + + # handle old code that relied on this class being a singleton + def instance + self + end + + # increments the active log level + def cycle_level + @logger.cycle_level if @configured + end + + def config_and_check_level(level) + configure unless @configured + check_level(level) + @logger.should_log?(level) + end + + def check_level(level) + raise "Unknown log level" unless valid_level?(level) + end + + def valid_level?(level) + VALID_LEVELS.include?(level) + end + + def message_for(msgid, args={}) + "%s: %s" % [msgid, Util.t(msgid, args)] + end + + def logexception(msgid, level, e, backtrace=false, args={}) + return false unless config_and_check_level(level) + + origin = File.basename(e.backtrace[1]) + + if e.is_a?(CodedError) + msg = "%s: %s" % [e.code, e.to_s] + else + error_string = "%s: %s" % [e.class, e.to_s] + msg = message_for(msgid, args.merge(:error => error_string)) + end + + log(level, msg, origin) + + if backtrace + e.backtrace.each do |line| + log(level, "%s: %s" % [msgid, line], origin) + end + end + end + + # Logs a message at a certain level, the message must be + # a token that will be looked up from the i18n localization + # database + # + # Messages can interprolate strings from the args hash, a + # message with "foo %{bar}" in the localization database + # will use args[:bar] for the value there, the interprolation + # is handled by the i18n library itself + def logmsg(msgid, default, level, args={}) + return false unless config_and_check_level(level) + + msg = message_for(msgid, {:default => default}.merge(args)) + + log(level, msg) + end + + # logs a message at a certain level + def log(level, msg, origin=nil) + return unless config_and_check_level(level) + + origin = from unless origin + + if @logger + @logger.log(level, origin, msg) + else + t = Time.new.strftime("%H:%M:%S") + + STDERR.puts "#{t}: #{level}: #{origin}: #{msg}" + end + end + + # sets the logger class to use + def set_logger(logger) + @logger = logger + end + + # configures the logger class, if the config has not yet been loaded + # we default to the console logging class and do not set @configured + # so that future calls to the log method will keep attempting to configure + # the logger till we eventually get a logging preference from the config + # module + def configure(logger=nil) + unless logger + logger_type = "console" + + config = Config.instance + + if config.configured + logger_type = config.logger_type + @configured = true + end + + require "mcollective/logger/%s_logger" % logger_type.downcase + + logger_class = MCollective::Logger.const_get("%s_logger" % logger_type.capitalize) + + set_logger(logger_class.new) + else + set_logger(logger) + @configured = true + end + + + @logger.start + rescue Exception => e + @configured = false + STDERR.puts "Could not start logger: #{e.class} #{e}" + end + + def unconfigure + @configured = false + set_logger(nil) + end + + # figures out the filename that called us + def from + from = File.basename(caller[2]) + end + end + end +end diff --git a/lib/mcollective/logger.rb b/lib/mcollective/logger.rb new file mode 100644 index 0000000..d41ad09 --- /dev/null +++ b/lib/mcollective/logger.rb @@ -0,0 +1,5 @@ +module MCollective + module Logger + autoload :Base, "mcollective/logger/base" + end +end diff --git a/lib/mcollective/logger/base.rb b/lib/mcollective/logger/base.rb new file mode 100644 index 0000000..9bdc530 --- /dev/null +++ b/lib/mcollective/logger/base.rb @@ -0,0 +1,77 @@ +module MCollective + module Logger + # A base class for logging providers. + # + # Logging providers should provide the following: + # + # * start - all you need to do to setup your logging + # * set_logging_level - set your logging to :info, :warn, etc + # * valid_levels - a hash of maps from :info to your internal level name + # * log - what needs to be done to log a specific message + class Base + attr_reader :active_level + + def initialize + @known_levels = [:debug, :info, :warn, :error, :fatal] + + # Sanity check the class that impliments the logging + @known_levels.each do |lvl| + raise "Logger class did not specify a map for #{lvl}" unless valid_levels.include?(lvl) + end + end + + def should_log?(level) + @known_levels.index(level) >= @known_levels.index(@active_level) + end + + # Figures out the next level and sets it + def cycle_level + lvl = get_next_level + set_level(lvl) + + log(lvl, "", "Logging level is now #{lvl.to_s.upcase}") + end + + # Sets a new level and record it in @active_level + def set_level(level) + set_logging_level(level) + @active_level = level.to_sym + end + + private + def map_level(level) + raise "Logger class do not know how to handle #{level} messages" unless valid_levels.include?(level.to_sym) + + valid_levels[level.to_sym] + end + + # Gets the next level in the list, cycles down to the firt once it reaches the end + def get_next_level + # if all else fails, always go to debug mode + nextlvl = :debug + + if @known_levels.index(@active_level) == (@known_levels.size - 1) + nextlvl = @known_levels.first + else + idx = @known_levels.index(@active_level) + 1 + nextlvl = @known_levels[idx] + end + + nextlvl + end + + # Abstract methods to ensure the logging implimentations supply what they should + def valid_levels + raise "The logging class did not supply a valid_levels method" + end + + def log(level, from, msg) + raise "The logging class did not supply a log method" + end + + def start + raise "The logging class did not supply a start method" + end + end + end +end diff --git a/lib/mcollective/logger/console_logger.rb b/lib/mcollective/logger/console_logger.rb new file mode 100644 index 0000000..9d5b8ea --- /dev/null +++ b/lib/mcollective/logger/console_logger.rb @@ -0,0 +1,59 @@ +module MCollective + module Logger + # Implements a syslog based logger using the standard ruby syslog class + class Console_logger :info, + :warn => :warning, + :debug => :debug, + :fatal => :crit, + :error => :err} + end + + def log(level, from, msg, normal_output=STDERR, last_resort_output=STDERR) + time = Time.new.strftime("%Y/%m/%d %H:%M:%S") + + normal_output.puts("%s %s: %s %s" % [colorize(level, level), time, from, msg]) + rescue + # if this fails we probably cant show the user output at all, + # STDERR it as last resort + last_resort_output.puts("#{level}: #{msg}") + end + + # Set some colors for various logging levels, will honor the + # color configuration option and return nothing if its configured + # not to + def color(level) + colorize = Config.instance.color + + colors = {:error => Util.color(:red), + :fatal => Util.color(:red), + :warn => Util.color(:yellow), + :info => Util.color(:green), + :reset => Util.color(:reset)} + + if colorize + return colors[level] || "" + else + return "" + end + end + + # Helper to return a string in specific color + def colorize(level, msg) + "%s%s%s" % [ color(level), msg, color(:reset) ] + end + end + end +end diff --git a/lib/mcollective/logger/file_logger.rb b/lib/mcollective/logger/file_logger.rb new file mode 100644 index 0000000..484ef42 --- /dev/null +++ b/lib/mcollective/logger/file_logger.rb @@ -0,0 +1,46 @@ +require 'logger' + +module MCollective + module Logger + # Impliments a file based logger using the standard ruby logger class + # + # To configure you should set: + # + # - config.logfile + # - config.keeplogs defaults to 2097152 + # - config.max_log_size defaults to 5 + class File_logger e + @logger.level = ::Logger::DEBUG + log(:error, "", "Could not set logging to #{level} using debug instead: #{e.class} #{e}") + end + + def valid_levels + {:info => ::Logger::INFO, + :warn => ::Logger::WARN, + :debug => ::Logger::DEBUG, + :fatal => ::Logger::FATAL, + :error => ::Logger::ERROR} + end + + def log(level, from, msg) + @logger.add(map_level(level)) { "#{from} #{msg}" } + rescue + # if this fails we probably cant show the user output at all, + # STDERR it as last resort + STDERR.puts("#{level}: #{msg}") + end + end + end +end diff --git a/lib/mcollective/logger/syslog_logger.rb b/lib/mcollective/logger/syslog_logger.rb new file mode 100644 index 0000000..af9e46a --- /dev/null +++ b/lib/mcollective/logger/syslog_logger.rb @@ -0,0 +1,51 @@ +module MCollective + module Logger + # Implements a syslog based logger using the standard ruby syslog class + class Syslog_logger e + STDERR.puts "Invalid syslog facility #{facility} supplied, reverting to USER" + Syslog::LOG_USER + end + end + + def set_logging_level(level) + # noop + end + + def valid_levels + {:info => :info, + :warn => :warning, + :debug => :debug, + :fatal => :crit, + :error => :err} + end + + def log(level, from, msg) + Syslog.send(map_level(level), "#{from} #{msg}") + rescue + # if this fails we probably cant show the user output at all, + # STDERR it as last resort + STDERR.puts("#{level}: #{msg}") + end + end + end +end diff --git a/lib/mcollective/matcher.rb b/lib/mcollective/matcher.rb new file mode 100644 index 0000000..d374776 --- /dev/null +++ b/lib/mcollective/matcher.rb @@ -0,0 +1,183 @@ +module MCollective + # A parser and scanner that creates a stack machine for a simple + # fact and class matching language used on the CLI to facilitate + # a rich discovery language + # + # Language EBNF + # + # compound = ["("] expression [")"] {["("] expression [")"]} + # expression = [!|not]statement ["and"|"or"] [!|not] statement + # char = A-Z | a-z | < | > | => | =< | _ | - |* | / { A-Z | a-z | < | > | => | =< | _ | - | * | / | } + # int = 0|1|2|3|4|5|6|7|8|9{|0|1|2|3|4|5|6|7|8|9|0} + module Matcher + autoload :Parser, "mcollective/matcher/parser" + autoload :Scanner, "mcollective/matcher/scanner" + + # Helper creates a hash from a function call string + def self.create_function_hash(function_call) + func_hash = {} + f = "" + func_parts = function_call.split(/(!=|>=|<=|<|>|=)/) + func_hash["r_compare"] = func_parts.pop + func_hash["operator"] = func_parts.pop + func = func_parts.join + + # Deal with dots in function parameters and functions without dot values + if func.match(/^.+\(.*\)$/) + f = func + else + func_parts = func.split(".") + func_hash["value"] = func_parts.pop + f = func_parts.join(".") + end + + # Deal with regular expression matches + if func_hash["r_compare"] =~ /^\/.*\/$/ + func_hash["operator"] = "=~" if func_hash["operator"] == "=" + func_hash["operator"] = "!=~" if func_hash["operator"] == "!=" + func_hash["r_compare"] = Regexp.new(func_hash["r_compare"].gsub(/^\/|\/$/, "")) + # Convert = operators to == so they can be propperly evaluated + elsif func_hash["operator"] == "=" + func_hash["operator"] = "==" + end + + # Grab function name and parameters from left compare string + func_hash["name"], func_hash["params"] = f.split("(") + if func_hash["params"] == ")" + func_hash["params"] = nil + else + + # Walk the function parameters from the front and from the + # back removing the first and last instances of single of + # double qoutes. We do this to handle the case where params + # contain escaped qoutes. + func_hash["params"] = func_hash["params"].gsub(")", "") + func_quotes = func_hash["params"].split(/('|")/) + + func_quotes.each_with_index do |item, i| + if item.match(/'|"/) + func_quotes.delete_at(i) + break + end + end + + func_quotes.reverse.each_with_index do |item,i| + if item.match(/'|"/) + func_quotes.delete_at(func_quotes.size - i - 1) + break + end + end + + func_hash["params"] = func_quotes.join + end + + func_hash + end + + # Returns the result of an executed function + def self.execute_function(function_hash) + # In the case where a data plugin isn't present there are two ways we can handle + # the raised exception. The function result can either be false or the entire + # expression can fail. + # + # In the case where we return the result as false it opens us op to unexpected + # negation behavior. + # + # !foo('bar').name = bar + # + # In this case the user would expect discovery to match on all machines where + # the name value of the foo function does not equal bar. If a non existent function + # returns false then it is posible to match machines where the name value of the + # foo function is bar. + # + # Instead we raise a DDLValidationError to prevent this unexpected behavior from + # happening. + + result = Data.send(function_hash["name"], function_hash["params"]) + + if function_hash["value"] + eval_result = result.send(function_hash["value"]) + return eval_result + else + return result + end + rescue NoMethodError + Log.debug("cannot execute discovery function '#{function_hash["name"]}'. data plugin not found") + raise DDLValidationError + end + + # Evaluates a compound statement + def self.eval_compound_statement(expression) + if expression.values.first =~ /^\// + return Util.has_cf_class?(expression.values.first) + elsif expression.values.first =~ />=|<=|=|<|>/ + optype = expression.values.first.match(/>=|<=|=|<|>/) + name, value = expression.values.first.split(optype[0]) + unless value.split("")[0] == "/" + optype[0] == "=" ? optype = "==" : optype = optype[0] + else + optype = "=~" + end + + return Util.has_fact?(name,value, optype).to_s + else + return Util.has_cf_class?(expression.values.first) + end + end + + # Returns the result of an evaluated compound statement that + # includes a function + def self.eval_compound_fstatement(function_hash) + l_compare = execute_function(function_hash) + + # Prevent unwanted discovery by limiting comparison operators + # on Strings and Booleans + if((l_compare.is_a?(String) || l_compare.is_a?(TrueClass) || l_compare.is_a?(FalseClass)) && function_hash["operator"].match(/<|>/)) + Log.debug "Cannot do > and < comparison on Booleans and Strings '#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}'" + return false + end + + # Prevent backticks in function parameters + if function_hash["params"] =~ /`/ + Log.debug("Cannot use backticks in function parameters") + return false + end + + # Escape strings for evaluation + function_hash["r_compare"] = "\"#{function_hash["r_compare"]}\"" if(l_compare.is_a?(String) && !(function_hash["operator"] =~ /=~|!=~/)) + + # Do a regex comparison if right compare string is a regex + if function_hash["operator"] =~ /(=~|!=~)/ + # Fail if left compare value isn't a string + unless l_compare.is_a?(String) + Log.debug("Cannot do a regex check on a non string value.") + return false + else + compare_result = l_compare.match(function_hash["r_compare"]) + # Flip return value for != operator + if function_hash["operator"] == "!=~" + !((compare_result.nil?) ? false : true) + else + (compare_result.nil?) ? false : true + end + end + # Otherwise evaluate the logical comparison + else + l_compare = "\"#{l_compare}\"" if l_compare.is_a?(String) + result = eval("#{l_compare} #{function_hash["operator"]} #{function_hash["r_compare"]}") + (result.nil?) ? false : result + end + end + + # Creates a callstack to be evaluated from a compound evaluation string + def self.create_compound_callstack(call_string) + callstack = Matcher::Parser.new(call_string).execution_stack + callstack.each_with_index do |statement, i| + if statement.keys.first == "fstatement" + callstack[i]["fstatement"] = create_function_hash(statement.values.first) + end + end + callstack + end + end +end diff --git a/lib/mcollective/matcher/parser.rb b/lib/mcollective/matcher/parser.rb new file mode 100644 index 0000000..83c9f62 --- /dev/null +++ b/lib/mcollective/matcher/parser.rb @@ -0,0 +1,128 @@ +module MCollective + module Matcher + class Parser + attr_reader :scanner, :execution_stack + + def initialize(args) + @scanner = Scanner.new(args) + @execution_stack = [] + @parse_errors = [] + @token_errors = [] + @paren_errors = [] + parse + exit_with_token_errors if @token_errors.size > 0 + exit_with_parse_errors if @parse_errors.size > 0 + exit_with_paren_errors if @paren_errors.size > 0 + end + + # Exit and highlight any malformed tokens + def exit_with_token_errors + @token_errors.each do |error_range| + (error_range[0]..error_range[1]).each do |i| + @scanner.arguments[i] = Util.colorize(:red, @scanner.arguments[i]) + end + end + raise "Malformed token(s) found while parsing -S input #{@scanner.arguments.join}" + end + + def exit_with_parse_errors + @parse_errors.each do |error_range| + (error_range[0]..error_range[1]).each do |i| + @scanner.arguments[i] = Util.colorize(:red, @scanner.arguments[i]) + end + end + raise "Parse errors found while parsing -S input #{ @scanner.arguments.join}" + end + + def exit_with_paren_errors + @paren_errors.each do |i| + @scanner.arguments[i] = Util.colorize(:red, @scanner.arguments[i]) + end + raise "Missing parenthesis found while parsing -S input #{@scanner.arguments.join}" + end + + # Parse the input string, one token at a time a contruct the call stack + def parse + pre_index = @scanner.token_index + p_token,p_token_value = nil + c_token,c_token_value = @scanner.get_token + parenth = 0 + + while (c_token != nil) + @scanner.token_index += 1 + n_token, n_token_value = @scanner.get_token + + unless n_token == " " + case c_token + when "bad_token" + @token_errors << c_token_value + + when "and" + unless (n_token =~ /not|fstatement|statement|\(/) || (scanner.token_index == scanner.arguments.size) && !(n_token == nil) + @parse_errors << [pre_index, scanner.token_index] + end + + if p_token == nil + @parse_errors << [pre_index - c_token.size, scanner.token_index] + elsif (p_token == "and" || p_token == "or") + @parse_errors << [pre_index - 1 - p_token.size, pre_index - 1] + end + + when "or" + unless (n_token =~ /not|fstatement|statement|\(/) || (scanner.token_index == scanner.arguments.size) && !(n_token == nil) + @parse_errors << [pre_index, scanner.token_index] + end + + if p_token == nil + @parse_errors << [pre_index - c_token.size, scanner.token_index] + elsif (p_token == "and" || p_token == "or") + @parse_errors << [pre_index - 1 - p_token.size, pre_index - 1] + end + + when "not" + unless n_token =~ /fstatement|statement|\(|not/ && !(n_token == nil) + @parse_errors << [pre_index, scanner.token_index] + end + + when "statement","fstatement" + unless n_token =~ /and|or|\)/ + unless scanner.token_index == scanner.arguments.size + @parse_errors << [pre_index, scanner.token_index] + end + end + + when ")" + unless (n_token =~ /|and|or|not|\(/) + unless(scanner.token_index == scanner.arguments.size) + @parse_errors << [pre_index, scanner.token_index] + end + end + unless @paren_errors.empty? + @paren_errors.pop + else + @paren_errors.push((n_token.nil?) ? scanner.token_index - 1: scanner.token_index - n_token_value.size) + end + + when "(" + unless n_token =~ /fstatement|statement|not|\(/ + @parse_errors << [pre_index, scanner.token_index] + end + @paren_errors.push((n_token.nil?) ? scanner.token_index - 1: scanner.token_index - n_token_value.size) + + else + @parse_errors << [pre_index, scanner.token_index] + end + + unless n_token == " " ||c_token == "bad_token" + @execution_stack << {c_token => c_token_value} + end + + p_token, p_token_value = c_token, c_token_value + c_token, c_token_value = n_token, n_token_value + end + pre_index = @scanner.token_index + end + end + end + end +end diff --git a/lib/mcollective/matcher/scanner.rb b/lib/mcollective/matcher/scanner.rb new file mode 100644 index 0000000..5d81785 --- /dev/null +++ b/lib/mcollective/matcher/scanner.rb @@ -0,0 +1,201 @@ +module MCollective + module Matcher + class Scanner + attr_accessor :arguments, :token_index + + def initialize(arguments) + @token_index = 0 + @arguments = arguments.split("") + @seperation_counter = 0 + @white_spaces = 0 + end + + # Scans the input string and identifies single language tokens + def get_token + if @token_index >= @arguments.size + return nil + end + + case @arguments[@token_index] + when "(" + return "(", "(" + + when ")" + return ")", ")" + + when "n" + if (@arguments[@token_index + 1] == "o") && (@arguments[@token_index + 2] == "t") && ((@arguments[@token_index + 3] == " ") || (@arguments[@token_index + 3] == "(")) + @token_index += 2 + return "not", "not" + else + gen_statement + end + + when "!" + return "not", "not" + + when "a" + if (@arguments[@token_index + 1] == "n") && (@arguments[@token_index + 2] == "d") && ((@arguments[@token_index + 3] == " ") || (@arguments[@token_index + 3] == "(")) + @token_index += 2 + return "and", "and" + else + gen_statement + end + + when "o" + if (@arguments[@token_index + 1] == "r") && ((@arguments[@token_index + 2] == " ") || (@arguments[@token_index + 2] == "(")) + @token_index += 1 + return "or", "or" + else + gen_statement + end + + when " " + return " ", " " + + else + gen_statement + end + end + + private + # Helper generates a statement token + def gen_statement + func = false + current_token_value = "" + j = @token_index + + begin + if (@arguments[j] == "/") + begin + current_token_value << @arguments[j] + j += 1 + end until (j >= @arguments.size) || (@arguments[j] =~ /\s/) + elsif (@arguments[j] =~ /=|<|>/) + while !(@arguments[j] =~ /=|<|>/) + current_token_value << @arguments[j] + j += 1 + end + + current_token_value << @arguments[j] + j += 1 + + if @arguments[j] == "/" + begin + current_token_value << @arguments[j] + j += 1 + if @arguments[j] == "/" + current_token_value << "/" + break + end + end until (j >= @arguments.size) || (@arguments[j] =~ /\//) + else + while (j < @arguments.size) && ((@arguments[j] != " ") && (@arguments[j] != ")")) + current_token_value << @arguments[j] + j += 1 + end + end + else + begin + # Identify and tokenize regular expressions by ignoring everything between /'s + if @arguments[j] == '/' + current_token_value << '/' + j+=1 + while(j < @arguments.size && @arguments[j] != '/') + current_token_value << @arguments[j] + j += 1 + end + current_token_value << @arguments[j] if @arguments[j] + break + end + if @arguments[j+1] == "(" + func = true + be_greedy = true + end + current_token_value << @arguments[j] + if be_greedy + while !(j+1 >= @arguments.size) && @arguments[j] != ')' + j += 1 + current_token_value << @arguments[j] + end + j += 1 + be_greedy = false + else + j += 1 + end + if(@arguments[j] == ' ') + break if(is_klass?(j) && !(@arguments[j-1] =~ /=|<|>/)) + end + if( (@arguments[j] == ' ') && (@seperation_counter < 2) && !(current_token_value.match(/^.+(=|<|>).+$/)) ) + if((index = lookahead(j))) + j = index + end + end + end until (j >= @arguments.size) || (@arguments[j] =~ /\s|\)/) + @seperation_counter = 0 + end + rescue Exception => e + raise "An exception was raised while trying to tokenize '#{current_token_value} - #{e}'" + end + + @token_index += current_token_value.size + @white_spaces - 1 + @white_spaces = 0 + + # bar( + if current_token_value.match(/.+?\($/) + return "bad_token", [@token_index - current_token_value.size + 1, @token_index] + # /foo/=bar + elsif current_token_value.match(/^\/.+?\/(<|>|=).+/) + return "bad_token", [@token_index - current_token_value.size + 1, @token_index] + elsif current_token_value.match(/^.+?\/(<|>|=).+/) + return "bad_token", [@token_index - current_token_value.size + 1, @token_index] + else + if func + if current_token_value.match(/^.+?\((\s*(')[^']*(')\s*(,\s*(')[^']*('))*)?\)(\.[a-zA-Z0-9_]+)?((!=|<=|>=|=|>|<).+)?$/) || + current_token_value.match(/^.+?\((\s*(")[^"]*(")\s*(,\s*(")[^"]*("))*)?\)(\.[a-zA-Z0-9_]+)?((!=|<=|>=|=|>|<).+)?$/) + return "fstatement", current_token_value + else + return "bad_token", [@token_index - current_token_value.size + 1, @token_index] + end + else + slash_err = false + current_token_value.split('').each do |c| + if c == '/' + slash_err = !slash_err + end + end + return "bad_token", [@token_index - current_token_value.size + 1, @token_index] if slash_err + return "statement", current_token_value + end + end + end + + # Deal with special puppet class statement + def is_klass?(j) + while(j < @arguments.size && @arguments[j] == ' ') + j += 1 + end + + if @arguments[j] =~ /=|<|>/ + return false + else + return true + end + end + + # Eat spaces while looking for the next comparison symbol + def lookahead(index) + index += 1 + while(index <= @arguments.size) + @white_spaces += 1 + unless(@arguments[index] =~ /\s/) + @seperation_counter +=1 + return index + end + index += 1 + end + return nil + end + end + end +end diff --git a/lib/mcollective/message.rb b/lib/mcollective/message.rb new file mode 100644 index 0000000..f9b5e56 --- /dev/null +++ b/lib/mcollective/message.rb @@ -0,0 +1,242 @@ +module MCollective + # container for a message, its headers, agent, collective and other meta data + class Message + attr_reader :message, :request, :validated, :msgtime, :payload, :type, :expected_msgid, :reply_to + attr_accessor :headers, :agent, :collective, :filter + attr_accessor :requestid, :discovered_hosts, :options, :ttl + + VALIDTYPES = [:message, :request, :direct_request, :reply] + + # payload - the message body without headers etc, just the text + # message - the original message received from the middleware + # options[:base64] - if the body base64 encoded? + # options[:agent] - the agent the message is for/from + # options[:collective] - the collective its for/from + # options[:headers] - the message headers + # options[:type] - an indicator about the type of message, :message, :request, :direct_request or :reply + # options[:request] - if this is a reply this should old the message we are replying to + # options[:filter] - for requests, the filter to encode into the message + # options[:options] - the normal client options hash + # options[:ttl] - the maximum amount of seconds this message can be valid for + # options[:expected_msgid] - in the case of replies this is the msgid it is expecting in the replies + # options[:requestid] - specific request id to use else one will be generated + def initialize(payload, message, options = {}) + options = {:base64 => false, + :agent => nil, + :headers => {}, + :type => :message, + :request => nil, + :filter => Util.empty_filter, + :options => {}, + :ttl => 60, + :expected_msgid => nil, + :requestid => nil, + :collective => nil}.merge(options) + + @payload = payload + @message = message + @requestid = options[:requestid] + @discovered_hosts = nil + @reply_to = nil + + @type = options[:type] + @headers = options[:headers] + @base64 = options[:base64] + @filter = options[:filter] + @expected_msgid = options[:expected_msgid] + @options = options[:options] + + @ttl = @options[:ttl] || Config.instance.ttl + @msgtime = 0 + + @validated = false + + if options[:request] + @request = options[:request] + @agent = request.agent + @collective = request.collective + @type = :reply + else + @agent = options[:agent] + @collective = options[:collective] + end + + base64_decode! + end + + # Sets the message type to one of the known types. In the case of :direct_request + # the list of hosts to communicate with should have been set with #discovered_hosts + # else an exception will be raised. This is for extra security, we never accidentally + # want to send a direct request without a list of hosts or something weird like that + # as it might result in a filterless broadcast being sent. + # + # Additionally you simply cannot set :direct_request if direct_addressing was not enabled + # this is to force a workflow that doesnt not yield in a mistake when someone might assume + # direct_addressing is enabled when its not. + def type=(type) + raise "Unknown message type #{type}" unless VALIDTYPES.include?(type) + + if type == :direct_request + raise "Direct requests is not enabled using the direct_addressing config option" unless Config.instance.direct_addressing + + unless @discovered_hosts && !@discovered_hosts.empty? + raise "Can only set type to :direct_request if discovered_hosts have been set" + end + + # clear out the filter, custom discovery sources might interpret the filters + # different than the remote mcollectived and in directed mode really the only + # filter that matters is the agent filter + @filter = Util.empty_filter + @filter["agent"] << @agent + end + + @type = type + end + + # Sets a custom reply-to target for requests. The connector plugin should inspect this + # when constructing requests and set this header ensuring replies will go to the custom target + # otherwise the connector should just do what it usually does + def reply_to=(target) + raise "Custom reply targets can only be set on requests" unless [:request, :direct_request].include?(@type) + + @reply_to = target + end + + # in the case of reply messages we are expecting replies to a previously + # created message. This stores a hint to that previously sent message id + # and can be used by other classes like the security plugins as a means + # of optimizing their behavior like by ignoring messages not directed + # at us. + def expected_msgid=(msgid) + raise "Can only store the expected msgid for reply messages" unless @type == :reply + @expected_msgid = msgid + end + + def base64_decode! + return unless @base64 + + @payload = SSL.base64_decode(@payload) + @base64 = false + end + + def base64_encode! + return if @base64 + + @payload = SSL.base64_encode(@payload) + @base64 = true + end + + def base64? + @base64 + end + + def encode! + case type + when :reply + raise "Cannot encode a reply message if no request has been associated with it" unless request + raise 'callerid in original request is not valid, surpressing reply to potentially forged request' unless PluginManager["security_plugin"].valid_callerid?(request.payload[:callerid]) + + @requestid = request.payload[:requestid] + @payload = PluginManager["security_plugin"].encodereply(agent, payload, requestid, request.payload[:callerid]) + when :request, :direct_request + validate_compount_filter(@filter["compound"]) unless @filter["compound"].empty? + + @requestid = create_reqid unless @requestid + @payload = PluginManager["security_plugin"].encoderequest(Config.instance.identity, payload, requestid, filter, agent, collective, ttl) + else + raise "Cannot encode #{type} messages" + end + end + + def validate_compount_filter(compound_filter) + compound_filter.each do |filter| + filter.each do |statement| + if statement["fstatement"] + functionname = statement["fstatement"]["name"] + pluginname = Data.pluginname(functionname) + value = statement["fstatement"]["value"] + + begin + ddl = DDL.new(pluginname, :data) + rescue + raise DDLValidationError, "Could not find DDL for data plugin #{pluginname}, cannot use #{functionname}() in discovery" + end + + # parses numbers and booleans entered as strings into proper + # types of data so that DDL validation will pass + statement["fstatement"]["params"] = Data.ddl_transform_input(ddl, statement["fstatement"]["params"]) + + Data.ddl_validate(ddl, statement["fstatement"]["params"]) + + unless value && Data.ddl_has_output?(ddl, value) + raise DDLValidationError, "#{functionname}() does not return a #{value} value" + end + end + end + end + end + + def decode! + raise "Cannot decode message type #{type}" unless [:request, :reply].include?(type) + + @payload = PluginManager["security_plugin"].decodemsg(self) + + if type == :request + raise 'callerid in request is not valid, surpressing reply to potentially forged request' unless PluginManager["security_plugin"].valid_callerid?(payload[:callerid]) + end + + [:collective, :agent, :filter, :requestid, :ttl, :msgtime].each do |prop| + instance_variable_set("@#{prop}", payload[prop]) if payload.include?(prop) + end + end + + # Perform validation against the message by checking filters and ttl + def validate + raise "Can only validate request messages" unless type == :request + + msg_age = Time.now.utc.to_i - msgtime + + if msg_age > ttl + cid = "" + cid += payload[:callerid] + "@" if payload.include?(:callerid) + cid += payload[:senderid] + + if msg_age > ttl + PluginManager["global_stats"].ttlexpired + + raise(MsgTTLExpired, "message #{requestid} from #{cid} created at #{msgtime} is #{msg_age} seconds old, TTL is #{ttl}") + end + end + + raise(NotTargettedAtUs, "Received message is not targetted to us") unless PluginManager["security_plugin"].validate_filter?(payload[:filter]) + + @validated = true + end + + # publish a reply message by creating a target name and sending it + def publish + Timeout.timeout(2) do + # If we've been specificaly told about hosts that were discovered + # use that information to do P2P calls if appropriate else just + # send it as is. + if @discovered_hosts && Config.instance.direct_addressing + if @discovered_hosts.size <= Config.instance.direct_addressing_threshold + self.type = :direct_request + Log.debug("Handling #{requestid} as a direct request") + end + + PluginManager["connector_plugin"].publish(self) + else + PluginManager["connector_plugin"].publish(self) + end + end + end + + def create_reqid + # we gsub out the -s so that the format of the id does not + # change from previous versions, these should just be more + # unique than previous ones + SSL.uuid.gsub("-", "") + end + end +end diff --git a/lib/mcollective/monkey_patches.rb b/lib/mcollective/monkey_patches.rb new file mode 100644 index 0000000..fbf72e3 --- /dev/null +++ b/lib/mcollective/monkey_patches.rb @@ -0,0 +1,121 @@ +# start_with? was introduced in 1.8.7, we need to support +# 1.8.5 and 1.8.6 +class String + def start_with?(str) + return self[0 .. (str.length-1)] == str + end unless method_defined?("start_with?") +end + +# Make arrays of Symbols sortable +class Symbol + include Comparable + + def <=>(other) + self.to_s <=> other.to_s + end unless method_defined?("<=>") +end + +# This provides an alias for RbConfig to Config for versions of Ruby older then +# # version 1.8.5. This allows us to use RbConfig in place of the older Config in +# # our code and still be compatible with at least Ruby 1.8.1. +# require 'rbconfig' +unless defined? ::RbConfig + ::RbConfig = ::Config +end + +# a method # that walks an array in groups, pass a block to +# call the block on each sub array +class Array + def in_groups_of(chunk_size, padded_with=nil, &block) + arr = self.clone + + # how many to add + padding = chunk_size - (arr.size % chunk_size) + + # pad at the end + arr.concat([padded_with] * padding) unless padding == chunk_size + + # how many chunks we'll make + count = arr.size / chunk_size + + # make that many arrays + result = [] + count.times {|s| result << arr[s * chunk_size, chunk_size]} + + if block_given? + result.each_with_index do |a, i| + case block.arity + when 1 + yield(a) + when 2 + yield(a, (i == result.size - 1)) + else + raise "Expected 1 or 2 arguments, got #{block.arity}" + end + end + else + result + end + end unless method_defined?(:in_groups_of) +end + +class String + def bytes(&block) + # This should not be necessary, really ... + require 'enumerator' + return to_enum(:each_byte) unless block_given? + each_byte(&block) + end unless method_defined?(:bytes) +end + +class Dir + def self.mktmpdir(prefix_suffix=nil, tmpdir=nil) + case prefix_suffix + when nil + prefix = "d" + suffix = "" + when String + prefix = prefix_suffix + suffix = "" + when Array + prefix = prefix_suffix[0] + suffix = prefix_suffix[1] + else + raise ArgumentError, "unexpected prefix_suffix: #{prefix_suffix.inspect}" + end + tmpdir ||= Dir.tmpdir + t = Time.now.strftime("%Y%m%d") + n = nil + begin + path = "#{tmpdir}/#{prefix}#{t}-#{$$}-#{rand(0x100000000).to_s(36)}" + path << "-#{n}" if n + path << suffix + Dir.mkdir(path, 0700) + rescue Errno::EEXIST + n ||= 0 + n += 1 + retry + end + + if block_given? + begin + yield path + ensure + FileUtils.remove_entry_secure path + end + else + path + end + end unless method_defined?(:mktmpdir) + + def self.tmpdir + tmp = '.' + for dir in [ENV['TMPDIR'], ENV['TMP'], ENV['TEMP'], '/tmp'] + if dir and stat = File.stat(dir) and stat.directory? and stat.writable? + tmp = dir + break + end rescue nil + end + File.expand_path(tmp) + end unless method_defined?(:tmpdir) +end diff --git a/lib/mcollective/optionparser.rb b/lib/mcollective/optionparser.rb new file mode 100644 index 0000000..fdb02cc --- /dev/null +++ b/lib/mcollective/optionparser.rb @@ -0,0 +1,181 @@ +module MCollective + # A simple helper to build cli tools that supports a uniform command line + # layout. + class Optionparser + attr_reader :parser + + # Creates a new instance of the parser, you can supply defaults and include named groups of options. + # + # Starts a parser that defaults to verbose and that includs the filter options: + # + # oparser = MCollective::Optionparser.new({:verbose => true}, "filter") + # + # Stats a parser in non verbose mode that does support discovery + # + # oparser = MCollective::Optionparser.new() + # + # Starts a parser in verbose mode that does not show the common options: + # + # oparser = MCollective::Optionparser.new({:verbose => true}, "filter", "common") + def initialize(defaults = {}, include_sections = nil, exclude_sections = nil) + @parser = ::OptionParser.new + + @include = [include_sections].flatten + @exclude = [exclude_sections].flatten + + @options = Util.default_options + + @options.merge!(defaults) + end + + # Parse the options returning the options, you can pass a block that adds additional options + # to the Optionparser. + # + # The sample below starts a parser that also prompts for --arguments in addition to the defaults. + # It also sets the description and shows a usage message specific to this app. + # + # options = oparser.parse{|parser, options| + # parser.define_head "Control the mcollective controller daemon" + # parser.banner = "Usage: sh-mcollective [options] command" + # + # parser.on('--arg', '--argument ARGUMENT', 'Argument to pass to agent') do |v| + # options[:argument] = v + # end + # } + # + # Users can set default options that get parsed in using the MCOLLECTIVE_EXTRA_OPTS environemnt + # variable + def parse(&block) + yield(@parser, @options) if block_given? + + add_required_options + + add_common_options unless @exclude.include?("common") + + @include.each do |i| + next if @exclude.include?(i) + + options_name = "add_#{i}_options" + send(options_name) if respond_to?(options_name) + end + + @parser.environment("MCOLLECTIVE_EXTRA_OPTS") + + @parser.parse! + + @options[:collective] = Config.instance.main_collective unless @options[:collective] + + @options + end + + # These options will be added if you pass 'filter' into the include list of the + # constructor. + def add_filter_options + @parser.separator "" + @parser.separator "Host Filters" + + @parser.on('-W', '--with FILTER', 'Combined classes and facts filter') do |f| + f.split(" ").each do |filter| + begin + fact_parsed = parse_fact(filter) + @options[:filter]["fact"] << fact_parsed + rescue + @options[:filter]["cf_class"] << filter + end + end + end + + @parser.on('-S', '--select FILTER', 'Compound filter combining facts and classes') do |f| + @options[:filter]["compound"] << Matcher.create_compound_callstack(f) + end + + @parser.on('-F', '--wf', '--with-fact fact=val', 'Match hosts with a certain fact') do |f| + fact_parsed = parse_fact(f) + + @options[:filter]["fact"] << fact_parsed if fact_parsed + end + + @parser.on('-C', '--wc', '--with-class CLASS', 'Match hosts with a certain config management class') do |f| + @options[:filter]["cf_class"] << f + end + + @parser.on('-A', '--wa', '--with-agent AGENT', 'Match hosts with a certain agent') do |a| + @options[:filter]["agent"] << a + end + + @parser.on('-I', '--wi', '--with-identity IDENT', 'Match hosts with a certain configured identity') do |a| + @options[:filter]["identity"] << a + end + end + + # These options should always be present + def add_required_options + @parser.on('-c', '--config FILE', 'Load configuratuion from file rather than default') do |f| + @options[:config] = f + end + + @parser.on('-v', '--verbose', 'Be verbose') do |v| + @options[:verbose] = v + end + + @parser.on('-h', '--help', 'Display this screen') do + puts @parser + exit! 1 + end + end + + # These options will be added to most cli tools + def add_common_options + @parser.separator "" + @parser.separator "Common Options" + + @parser.on('-T', '--target COLLECTIVE', 'Target messages to a specific sub collective') do |f| + @options[:collective] = f + end + + @parser.on('--dt', '--discovery-timeout SECONDS', Integer, 'Timeout for doing discovery') do |t| + @options[:disctimeout] = t + end + + @parser.on('-t', '--timeout SECONDS', Integer, 'Timeout for calling remote agents') do |t| + @options[:timeout] = t + end + + @parser.on('-q', '--quiet', 'Do not be verbose') do |v| + @options[:verbose] = false + end + + @parser.on('--ttl TTL', 'Set the message validity period') do |v| + @options[:ttl] = v.to_i + end + + @parser.on('--reply-to TARGET', 'Set a custom target for replies') do |v| + @options[:reply_to] = v + end + + @parser.on('--dm', '--disc-method METHOD', 'Which discovery method to use') do |v| + raise "Discovery method is already set by a competing option" if @options[:discovery_method] && @options[:discovery_method] != v + @options[:discovery_method] = v + end + + @parser.on('--do', '--disc-option OPTION', 'Options to pass to the discovery method') do |a| + @options[:discovery_options] << a + end + + @parser.on("--nodes FILE", "List of nodes to address") do |v| + raise "Cannot mix --disc-method, --disc-option and --nodes" if @options[:discovery_method] || @options[:discovery_options].size > 0 + raise "Cannot read the discovery file #{v}" unless File.readable?(v) + + @options[:discovery_method] = "flatfile" + @options[:discovery_options] = v + end + end + + private + # Parse a fact filter string like foo=bar into the tuple hash thats needed + def parse_fact(fact) + Util.parse_fact_string(fact) + end + + end +end diff --git a/lib/mcollective/pluginmanager.rb b/lib/mcollective/pluginmanager.rb new file mode 100644 index 0000000..77e5150 --- /dev/null +++ b/lib/mcollective/pluginmanager.rb @@ -0,0 +1,180 @@ +module MCollective + # A simple plugin manager, it stores one plugin each of a specific type + # the idea is that we can only have one security provider, one connector etc. + module PluginManager + @plugins = {} + + # Adds a plugin to the list of plugins, we expect a hash like: + # + # {:type => "base", + # :class => foo.new} + # + # or like: + # {:type => "base", + # :class => "Foo::Bar"} + # + # In the event that we already have a class with the given type + # an exception will be raised. + # + # If the :class passed is a String then we will delay instantiation + # till the first time someone asks for the plugin, this is because most likely + # the registration gets done by inherited() hooks, at which point the plugin class is not final. + # + # If we were to do a .new here the Class initialize method would get called and not + # the plugins, we there for only initialize the classes when they get requested via [] + # + # By default all plugin instances are cached and returned later so there's + # always a single instance. You can pass :single_instance => false when + # calling this to instruct it to always return a new instance when a copy + # is requested. This only works with sending a String for :class. + def self.<<(plugin) + plugin[:single_instance] = true unless plugin.include?(:single_instance) + + type = plugin[:type] + klass = plugin[:class] + single = plugin[:single_instance] + + raise("Plugin #{type} already loaded") if @plugins.include?(type) + + + # If we get a string then store 'nil' as the instance, signalling that we'll + # create the class later on demand. + if klass.is_a?(String) + @plugins[type] = {:loadtime => Time.now, :class => klass, :instance => nil, :single => single} + Log.debug("Registering plugin #{type} with class #{klass} single_instance: #{single}") + else + @plugins[type] = {:loadtime => Time.now, :class => klass.class, :instance => klass, :single => true} + Log.debug("Registering plugin #{type} with class #{klass.class} single_instance: true") + end + end + + # Removes a plugim the list + def self.delete(plugin) + @plugins.delete(plugin) if @plugins.include?(plugin) + end + + # Finds out if we have a plugin with the given name + def self.include?(plugin) + @plugins.include?(plugin) + end + + # Provides a list of plugins we know about + def self.pluginlist + @plugins.keys.sort + end + + # deletes all registered plugins + def self.clear + @plugins.clear + end + + # Gets a plugin by type + def self.[](plugin) + raise("No plugin #{plugin} defined") unless @plugins.include?(plugin) + + klass = @plugins[plugin][:class] + + if @plugins[plugin][:single] + # Create an instance of the class if one hasn't been done before + if @plugins[plugin][:instance] == nil + Log.debug("Returning new plugin #{plugin} with class #{klass}") + @plugins[plugin][:instance] = create_instance(klass) + else + Log.debug("Returning cached plugin #{plugin} with class #{klass}") + end + + @plugins[plugin][:instance] + else + Log.debug("Returning new plugin #{plugin} with class #{klass}") + create_instance(klass) + end + end + + # use eval to create an instance of a class + def self.create_instance(klass) + begin + eval("#{klass}.new") + rescue Exception => e + raise("Could not create instance of plugin #{klass}: #{e}") + end + end + + # Finds plugins in all configured libdirs + # + # find("agent") + # + # will return an array of just agent names, for example: + # + # ["puppetd", "package"] + # + # Can also be used to find files of other extensions: + # + # find("agent", "ddl") + # + # Will return the same list but only of files with extension .ddl + # in the agent subdirectory + def self.find(type, extension="rb") + extension = ".#{extension}" unless extension.match(/^\./) + + plugins = [] + + Config.instance.libdir.each do |libdir| + plugdir = File.join([libdir, "mcollective", type.to_s]) + next unless File.directory?(plugdir) + + Dir.new(plugdir).grep(/#{extension}$/).map do |plugin| + plugins << File.basename(plugin, extension) + end + end + + plugins.sort.uniq + end + + # Finds and loads from disk all plugins from all libdirs that match + # certain criteria. + # + # find_and_load("pluginpackager") + # + # Will find all .rb files in the libdir/mcollective/pluginpackager/ + # directory in all libdirs and load them from disk. + # + # You can influence what plugins get loaded using a block notation: + # + # find_and_load("pluginpackager") do |plugin| + # plugin.match(/puppet/) + # end + # + # This will load only plugins matching /puppet/ + def self.find_and_load(type, extension="rb") + extension = ".#{extension}" unless extension.match(/^\./) + + klasses = find(type, extension).map do |plugin| + if block_given? + next unless yield(plugin) + end + + "%s::%s::%s" % [ "MCollective", type.capitalize, plugin.capitalize ] + end.compact + + klasses.sort.uniq.each {|klass| loadclass(klass, true)} + end + + # Loads a class from file by doing some simple search/replace + # on class names and then doing a require. + def self.loadclass(klass, squash_failures=false) + fname = klass.gsub("::", "/").downcase + ".rb" + + Log.debug("Loading #{klass} from #{fname}") + + load fname + rescue Exception => e + Log.error("Failed to load #{klass}: #{e}") + raise unless squash_failures + end + + # Grep's over the plugin list and returns the list found + def self.grep(regex) + @plugins.keys.grep(regex) + end + end +end diff --git a/lib/mcollective/pluginpackager.rb b/lib/mcollective/pluginpackager.rb new file mode 100644 index 0000000..d003090 --- /dev/null +++ b/lib/mcollective/pluginpackager.rb @@ -0,0 +1,68 @@ +module MCollective + module PluginPackager + # Plugin definition classes + autoload :AgentDefinition, "mcollective/pluginpackager/agent_definition" + autoload :StandardDefinition, "mcollective/pluginpackager/standard_definition" + + # Package implementation plugins + def self.load_packagers + PluginManager.find_and_load("pluginpackager") + end + + def self.[](klass) + const_get("#{klass}") + end + + # Fetch and return metadata from plugin DDL + def self.get_metadata(path, type) + ddl = DDL.new("package", type.to_sym, false) + + begin + ddl_file = File.read(Dir.glob(File.join(path, type, "*.ddl")).first) + rescue Exception + raise "failed to load ddl file in plugin directory : #{File.join(path, type)}" + end + ddl.instance_eval ddl_file + + return ddl.meta, ddl.requirements[:mcollective] + end + + # Checks if a directory is present and not empty + def self.check_dir_present(path) + (File.directory?(path) && !Dir.glob(File.join(path, "*")).empty?) + end + + # Quietly calls a block if verbose parameter is false + def self.do_quietly?(verbose, &block) + unless verbose + old_stdout = $stdout.clone + $stdout.reopen(File.new("/dev/null", "w")) + begin + block.call + rescue Exception => e + $stdout.reopen old_stdout + raise e + ensure + $stdout.reopen old_stdout + end + else + block.call + end + end + + # Checks if a build tool is present on the system + def self.build_tool?(build_tool) + ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| + builder = File.join(path, build_tool) + if File.exists?(builder) + return true + end + end + false + end + + def self.safe_system(*args) + raise RuntimeError, "Failed: #{args.join(' ')}" unless system *args + end + end +end diff --git a/lib/mcollective/pluginpackager/agent_definition.rb b/lib/mcollective/pluginpackager/agent_definition.rb new file mode 100644 index 0000000..d207a72 --- /dev/null +++ b/lib/mcollective/pluginpackager/agent_definition.rb @@ -0,0 +1,94 @@ +module MCollective + module PluginPackager + # MCollective Agent Plugin package + class AgentDefinition + attr_accessor :path, :packagedata, :metadata, :target_path, :vendor, :iteration, :preinstall + attr_accessor :plugintype, :dependencies, :postinstall, :mcname, :mcversion + + def initialize(path, name, vendor, preinstall, postinstall, iteration, dependencies, mcdependency, plugintype) + @plugintype = plugintype + @path = path + @packagedata = {} + @iteration = iteration || 1 + @preinstall = preinstall + @postinstall = postinstall + @vendor = vendor || "Puppet Labs" + @dependencies = dependencies || [] + @target_path = File.expand_path(@path) + @metadata, mcversion = PluginPackager.get_metadata(@path, "agent") + @mcname = mcdependency[:mcname] || "mcollective" + @mcversion = mcdependency[:mcversion] || mcversion + @dependencies << {:name => "#{@mcname}-common", :version => @mcversion} + + @metadata[:name] = (name || @metadata[:name]).downcase.gsub(/\s+|_/, "-") + identify_packages + end + + # Identify present packages and populate packagedata hash. + def identify_packages + common_package = common + @packagedata[:common] = common_package if common_package + agent_package = agent + @packagedata[:agent] = agent_package if agent_package + client_package = client + @packagedata[:client] = client_package if client_package + end + + # Obtain Agent package files and dependencies. + def agent + agent = {:files => [], + :dependencies => @dependencies.clone, + :description => "Agent plugin for #{@metadata[:name]}"} + + agentdir = File.join(@path, "agent") + + if PluginPackager.check_dir_present agentdir + ddls = Dir.glob(File.join(agentdir, "*.ddl")) + agent[:files] = (Dir.glob(File.join(agentdir, "*")) - ddls) + else + return nil + end + agent[:plugindependency] = {:name => "#{@mcname}-#{@metadata[:name]}-common", :version => @metadata[:version], :iteration => @iteration} + agent + end + + # Obtain client package files and dependencies. + def client + client = {:files => [], + :dependencies => @dependencies.clone, + :description => "Client plugin for #{@metadata[:name]}"} + + clientdir = File.join(@path, "application") + aggregatedir = File.join(@path, "aggregate") + + client[:files] += Dir.glob(File.join(clientdir, "*")) if PluginPackager.check_dir_present clientdir + client[:files] += Dir.glob(File.join(aggregatedir, "*")) if PluginPackager.check_dir_present aggregatedir + client[:plugindependency] = {:name => "#{@mcname}-#{@metadata[:name]}-common", :version => @metadata[:version], :iteration => @iteration} + client[:files].empty? ? nil : client + end + + # Obtain common package files and dependencies. + def common + common = {:files =>[], + :dependencies => @dependencies.clone, + :description => "Common libraries for #{@metadata[:name]}"} + + datadir = File.join(@path, "data", "**") + utildir = File.join(@path, "util", "**", "**") + ddldir = File.join(@path, "agent", "*.ddl") + validatordir = File.join(@path, "validator", "**") + + [datadir, utildir, validatordir, ddldir].each do |directory| + common[:files] += Dir.glob(directory) + end + + # We fail if there is no ddl file present + if common[:files].grep(/^.*\.ddl$/).empty? + raise "cannot create package - No ddl file found in #{File.join(@path, "agent")}" + end + + common[:files].empty? ? nil : common + end + end + end +end diff --git a/lib/mcollective/pluginpackager/standard_definition.rb b/lib/mcollective/pluginpackager/standard_definition.rb new file mode 100644 index 0000000..6c389a1 --- /dev/null +++ b/lib/mcollective/pluginpackager/standard_definition.rb @@ -0,0 +1,69 @@ +module MCollective + module PluginPackager + class StandardDefinition + attr_accessor :path, :packagedata, :metadata, :target_path, :vendor, :iteration + attr_accessor :plugintype, :preinstall, :postinstall, :dependencies, :mcname, :mcversion + + def initialize(path, name, vendor, preinstall, postinstall, iteration, dependencies, mcdependency, plugintype) + @plugintype = plugintype + @path = path + @packagedata = {} + @iteration = iteration || 1 + @preinstall = preinstall + @postinstall = postinstall + @vendor = vendor || "Puppet Labs" + @dependencies = dependencies || [] + @target_path = File.expand_path(@path) + @metadata, mcversion = PluginPackager.get_metadata(@path, @plugintype) + + @mcname = mcdependency[:mcname] || "mcollective" + @mcversion = mcdependency[:mcversion] || mcversion + @dependencies << {:name => "#{mcname}-common", :version => @mcversion} + @metadata[:name] = (name || @metadata[:name]).downcase.gsub(/\s+|_/, "-") + identify_packages + end + + # Identify present packages and populate the packagedata hash + def identify_packages + common_package = common + @packagedata[:common] = common_package if common_package + plugin_package = plugin + @packagedata[@plugintype] = plugin_package if plugin_package + end + + # Obtain standard plugin files and dependencies + def plugin + plugindata = {:files => [], + :dependencies => @dependencies.clone, + :description => "#{@name} #{@plugintype} plugin for the Marionette Collective."} + + plugindir = File.join(@path, @plugintype.to_s) + if PluginPackager.check_dir_present plugindir + plugindata[:files] = Dir.glob(File.join(plugindir, "*")) + else + return nil + end + + plugindata[:plugindependency] = {:name => "#{@mcname}-#{@metadata[:name]}-common", + :version => @metadata[:version], + :iteration => @iteration} if @packagedata[:common] + plugindata + end + + # Obtain list of common files + def common + common = {:files => [], + :dependencies => @dependencies.clone, + :description => "Common libraries for #{@name} connector plugin"} + + commondir = File.join(@path, "util") + if PluginPackager.check_dir_present commondir + common[:files] = Dir.glob(File.join(commondir, "*")) + return common + else + return nil + end + end + end + end +end diff --git a/lib/mcollective/registration.rb b/lib/mcollective/registration.rb new file mode 100644 index 0000000..98ddf1f --- /dev/null +++ b/lib/mcollective/registration.rb @@ -0,0 +1,16 @@ +module MCollective + # Registration is implimented using a module structure and installations can + # configure which module they want to use. + # + # We provide a simple one that just sends back the list of current known agents + # in MCollective::Registration::Agentlist, you can create your own: + # + # Create a module in plugins/mcollective/registration/.rb + # + # You can inherit from MCollective::Registration::Base in which case you just need + # to supply a _body_ method, whatever this method returns will be send to the + # middleware connection for an agent called _registration_ + module Registration + autoload :Base, "mcollective/registration/base" + end +end diff --git a/lib/mcollective/registration/base.rb b/lib/mcollective/registration/base.rb new file mode 100644 index 0000000..b1e7f14 --- /dev/null +++ b/lib/mcollective/registration/base.rb @@ -0,0 +1,77 @@ +module MCollective + module Registration + # This is a base class that other registration plugins can use + # to handle regular announcements to the mcollective + # + # The configuration file determines how often registration messages + # gets sent using the _registerinterval_ option, the plugin runs in the + # background in a thread. + class Base + # Register plugins that inherits base + def self.inherited(klass) + PluginManager << {:type => "registration_plugin", :class => klass.to_s} + end + + # Creates a background thread that periodically send a registration notice. + # + # The actual registration notices comes from the 'body' method of the registration + # plugins. + def run(connection) + return false if interval == 0 + + Thread.new do + loop do + begin + publish(body) + + sleep interval + rescue Exception => e + Log.error("Sending registration message failed: #{e}") + sleep interval + end + end + end + end + + def config + Config.instance + end + + def msg_filter + filter = Util.empty_filter + filter["agent"] << "registration" + filter + end + + def target_collective + main_collective = config.main_collective + + collective = config.registration_collective || main_collective + + unless config.collectives.include?(collective) + Log.warn("Sending registration to #{main_collective}: #{collective} is not a valid collective") + collective = main_collective + end + + return collective + end + + def interval + config.registerinterval + end + + def publish(message) + unless message + Log.debug("Skipping registration due to nil body") + else + req = Message.new(message, nil, {:type => :request, :agent => "registration", :collective => target_collective, :filter => msg_filter}) + req.encode! + + Log.debug("Sending registration #{req.requestid} to collective #{req.collective}") + + req.publish + end + end + end + end +end diff --git a/lib/mcollective/rpc.rb b/lib/mcollective/rpc.rb new file mode 100644 index 0000000..575d069 --- /dev/null +++ b/lib/mcollective/rpc.rb @@ -0,0 +1,182 @@ +require 'pp' + +module MCollective + # Toolset to create a standard interface of client and agent using + # an RPC metaphor, standard compliant agents will make it easier + # to create generic clients like web interfaces etc + module RPC + autoload :ActionRunner, "mcollective/rpc/actionrunner" + autoload :Agent, "mcollective/rpc/agent" + autoload :Audit, "mcollective/rpc/audit" + autoload :Client, "mcollective/rpc/client" + autoload :Helpers, "mcollective/rpc/helpers" + autoload :Progress, "mcollective/rpc/progress" + autoload :Reply, "mcollective/rpc/reply" + autoload :Request, "mcollective/rpc/request" + autoload :Result, "mcollective/rpc/result" + autoload :Stats, "mcollective/rpc/stats" + + # Creates a standard options hash, pass in a block to add extra headings etc + # see Optionparser + def rpcoptions + oparser = MCollective::Optionparser.new({:verbose => false, :progress_bar => true}, "filter") + + options = oparser.parse do |parser, options| + if block_given? + yield(parser, options) + end + + Helpers.add_simplerpc_options(parser, options) + end + + return options + end + + # Wrapper to create clients, supposed to be used as + # a mixin: + # + # include MCollective::RPC + # + # exim = rpcclient("exim") + # printrpc exim.mailq + # + # or + # + # rpcclient("exim") do |exim| + # printrpc exim.mailq + # end + # + # It will take a few flags: + # :configfile => "etc/client.cfg" + # :options => options + # :exit_on_failure => true + # + # Options would be a build up options hash from the Optionparser + # you can use the rpcoptions helper to create this + # + # :exit_on_failure is true by default, and causes the application to + # exit if there is a failure constructing the RPC client. Set this flag + # to false to cause an Exception to be raised instead. + def rpcclient(agent, flags = {}) + configfile = flags[:configfile] || "/etc/mcollective/client.cfg" + options = flags[:options] || nil + + if flags.key?(:exit_on_failure) + exit_on_failure = flags[:exit_on_failure] + else + # We exit on failure by default for CLI-friendliness + exit_on_failure = true + end + + begin + if options + rpc = Client.new(agent, :configfile => options[:config], :options => options) + @options = rpc.options + else + rpc = Client.new(agent, :configfile => configfile) + @options = rpc.options + end + rescue Exception => e + if exit_on_failure + puts("Could not create RPC client: #{e}") + exit! + else + raise e + end + end + + if block_given? + yield(rpc) + else + return rpc + end + end + + # means for other classes to drop stats into this module + # its a bit hacky but needed so that the mixin methods like + # printrpcstats can easily get access to it without + # users having to pass it around in params. + def self.stats(stats) + @@stats = stats + end + + # means for other classes to drop discovered hosts into this module + # its a bit hacky but needed so that the mixin methods like + # printrpcstats can easily get access to it without + # users having to pass it around in params. + def self.discovered(discovered) + @@discovered = discovered + end + + # Prints stats, requires stats to be saved from elsewhere + # using the MCollective::RPC.stats method. + # + # If you've passed -v on the command line a detailed stat block + # will be printed, else just a one liner. + # + # You can pass flags into it: + # + # printrpcstats :caption => "Foo", :summarize => true + # + # This will use "Foo" as the caption to the stats in verbose + # mode and print out any aggregate summary information if present + def printrpcstats(flags={}) + return unless @options[:output_format] == :console + + flags = {:summarize => false, :caption => "rpc stats"}.merge(flags) + + verbose = @options[:verbose] rescue verbose = false + + begin + stats = @@stats + rescue + puts("no stats to display") + return + end + + puts stats.report(flags[:caption], flags[:summarize], verbose) + end + + # Prints the result of an RPC call. + # + # In the default quiet mode - no flattening or verbose - only results + # that produce an error will be printed + # + # To get details of each result run with the -v command line option. + def printrpc(result, flags = {}) + verbose = @options[:verbose] rescue verbose = false + verbose = flags[:verbose] || verbose + flatten = flags[:flatten] || false + format = @options[:output_format] + forced_mode = @options[:force_display_mode] || false + + result_text = Helpers.rpcresults(result, {:verbose => verbose, :flatten => flatten, :format => format, :force_display_mode => forced_mode}) + + if result.is_a?(Array) && format == :console + puts "\n%s\n" % [ result_text ] + else + # when we get just one result to print dont pad them all with + # blank spaces etc, just print the individual result with no + # padding + puts result_text unless result_text == "" + end + end + + # Wrapper for MCollective::Util.empty_filter? to make clients less fugly + # to write - ticket #18 + def empty_filter?(options) + if options.include?(:filter) + Util.empty_filter?(options[:filter]) + else + Util.empty_filter?(options) + end + end + + def self.const_missing(const_name) + super unless const_name == :DDL + + Log.warn("MCollective::RPC::DDL is deprecatd, please use MCollective::DDL instead") + MCollective::DDL + end + end +end diff --git a/lib/mcollective/rpc/actionrunner.rb b/lib/mcollective/rpc/actionrunner.rb new file mode 100644 index 0000000..f2a65d5 --- /dev/null +++ b/lib/mcollective/rpc/actionrunner.rb @@ -0,0 +1,142 @@ +module MCollective + module RPC + # A helper used by RPC::Agent#implemented_by to delegate an action to + # an external script. At present only JSON based serialization is + # supported in future ones based on key=val pairs etc will be added + # + # It serializes the request object into an input file and creates an + # empty output file. It then calls the external command reading the + # output file at the end. + # + # any STDERR gets logged at error level and any STDOUT gets logged at + # info level. + # + # It will interpret the exit code from the application the same way + # RPC::Reply#fail! and #fail handles their codes creating a consistent + # interface, the message part of the fail message will come from STDERR + # + # Generally externals should just exit with code 1 on failure and print to + # STDERR, this is exactly what Perl die() does and translates perfectly + # to our model + # + # It uses the MCollective::Shell wrapper to call the external application + class ActionRunner + attr_reader :command, :agent, :action, :format, :stdout, :stderr, :request + + def initialize(command, request, format=:json) + @agent = request.agent + @action = request.action + @format = format + @request = request + @command = path_to_command(command) + @stdout = "" + @stderr = "" + end + + def run + unless canrun?(command) + Log.warn("Cannot run #{to_s}") + raise RPCAborted, "Cannot execute #{to_s}" + end + + Log.debug("Running #{to_s}") + + request_file = saverequest(request) + reply_file = tempfile("reply") + reply_file.close + + runner = shell(command, request_file.path, reply_file.path) + + runner.runcommand + + Log.debug("#{command} exited with #{runner.status.exitstatus}") + + stderr.each_line {|l| Log.error("#{to_s}: #{l.chomp}")} unless stderr.empty? + stdout.each_line {|l| Log.info("#{to_s}: #{l.chomp}")} unless stdout.empty? + + {:exitstatus => runner.status.exitstatus, + :stdout => runner.stdout, + :stderr => runner.stderr, + :data => load_results(reply_file.path)} + ensure + request_file.close! if request_file.respond_to?("close!") + reply_file.close! if reply_file.respond_to?("close") + end + + def shell(command, infile, outfile) + env = {"MCOLLECTIVE_REQUEST_FILE" => infile, + "MCOLLECTIVE_REPLY_FILE" => outfile} + + Shell.new("#{command} #{infile} #{outfile}", :cwd => Dir.tmpdir, :stdout => stdout, :stderr => stderr, :environment => env) + end + + def load_results(file) + Log.debug("Attempting to load results in #{format} format from #{file}") + + data = {} + + if respond_to?("load_#{format}_results") + tempdata = send("load_#{format}_results", file) + + tempdata.each_pair do |k,v| + data[k.to_sym] = v + end + end + + data + rescue Exception => e + {} + end + + def load_json_results(file) + return {} unless File.readable?(file) + + JSON.load(File.read(file)) || {} + rescue JSON::ParserError + {} + end + + def saverequest(req) + Log.debug("Attempting to save request in #{format} format") + + if respond_to?("save_#{format}_request") + data = send("save_#{format}_request", req) + + request_file = tempfile("request") + request_file.puts data + request_file.close + end + + request_file + end + + def save_json_request(req) + req.to_json + end + + def canrun?(command) + File.executable?(command) + end + + def to_s + "%s#%s command: %s" % [ agent, action, command ] + end + + def tempfile(prefix) + Tempfile.new("mcollective_#{prefix}", Dir.tmpdir) + end + + def path_to_command(command) + unless command[0,1] == File::SEPARATOR + Config.instance.libdir.each do |libdir| + command_file = File.join(libdir, "agent", agent, command) + + return command_file if File.exist?(command_file) + end + end + + return command + end + end + end +end diff --git a/lib/mcollective/rpc/agent.rb b/lib/mcollective/rpc/agent.rb new file mode 100644 index 0000000..d2cf043 --- /dev/null +++ b/lib/mcollective/rpc/agent.rb @@ -0,0 +1,375 @@ +module MCollective + module RPC + # A wrapper around the traditional agent, it takes care of a lot of the tedious setup + # you would do for each agent allowing you to just create methods following a naming + # standard leaving the heavy lifting up to this clas. + # + # See http://marionette-collective.org/simplerpc/agents.html + # + # It only really makes sense to use this with a Simple RPC client on the other end, basic + # usage would be: + # + # module MCollective + # module Agent + # class Helloworld e + DDL.validation_fail!(:PLMC24, "Failed to load DDL for the '%{agent}' agent, DDLs are required: %{error_class}: %{error}", :error, :agent => @agent_name, :error_class => e.class, :error => e.to_s) + end + + def handlemsg(msg, connection) + @request = RPC::Request.new(msg, @ddl) + @reply = RPC::Reply.new(@request.action, @ddl) + + begin + # Incoming requests need to be validated against the DDL thus reusing + # all the work users put into creating DDLs and creating a consistant + # quality of input validation everywhere with the a simple once off + # investment of writing a DDL + @request.validate! + + # Calls the authorization plugin if any is defined + # if this raises an exception we wil just skip processing this + # message + authorization_hook(@request) if respond_to?("authorization_hook") + + # Audits the request, currently continues processing the message + # we should make this a configurable so that an audit failure means + # a message wont be processed by this node depending on config + audit_request(@request, connection) + + before_processing_hook(msg, connection) + + if respond_to?("#{@request.action}_action") + send("#{@request.action}_action") + else + log_code(:PLMC36, "Unknown action '%{action}' for agent '%{agent}'", :warn, :action => @request.action, :agent => @request.agent) + raise UnknownRPCAction, "Unknown action '#{@request.action}' for agent '#{@request.agent}'" + end + rescue RPCAborted => e + @reply.fail e.to_s, 1 + + rescue UnknownRPCAction => e + @reply.fail e.to_s, 2 + + rescue MissingRPCData => e + @reply.fail e.to_s, 3 + + rescue InvalidRPCData, DDLValidationError => e + @reply.fail e.to_s, 4 + + rescue UnknownRPCError => e + Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s]) + Log.error(e.backtrace.join("\n\t")) + @reply.fail e.to_s, 5 + + rescue Exception => e + Log.error("%s#%s failed: %s: %s" % [@agent_name, @request.action, e.class, e.to_s]) + Log.error(e.backtrace.join("\n\t")) + @reply.fail e.to_s, 5 + + end + + after_processing_hook + + if @request.should_respond? + return @reply.to_hash + else + log_code(:PLMC35, "Client did not request a response, surpressing reply", :debug) + return nil + end + end + + # By default RPC Agents support a toggle in the configuration that + # can enable and disable them based on the agent name + # + # Example an agent called Foo can have: + # + # plugin.foo.activate_agent = false + # + # and this will prevent the agent from loading on this particular + # machine. + # + # Agents can use the activate_when helper to override this for example: + # + # activate_when do + # File.exist?("/usr/bin/puppet") + # end + def self.activate? + agent_name = self.to_s.split("::").last.downcase + + log_code(:PLMC37, "Starting default activation checks for the '%{agent}' agent", :debug, :agent => agent_name) + + should_activate = Config.instance.pluginconf["#{agent_name}.activate_agent"] + + if should_activate + log_code(:PLMC38, "Found plugin configuration '%{agent}.activate_agent' with value '%{should_activate}'", :debug, :agent => agent_name, :should_activate => should_activate) + + unless should_activate =~ /^1|y|true$/ + return false + end + end + + return true + end + + # Returns an array of actions this agent support + def self.actions + public_instance_methods.sort.grep(/_action$/).map do |method| + $1 if method =~ /(.+)_action$/ + end + end + + private + # Runs a command via the MC::Shell wrapper, options are as per MC::Shell + # + # The simplest use is: + # + # out = "" + # err = "" + # status = run("echo 1", :stdout => out, :stderr => err) + # + # reply[:out] = out + # reply[:error] = err + # reply[:exitstatus] = status + # + # This can be simplified as: + # + # reply[:exitstatus] = run("echo 1", :stdout => :out, :stderr => :error) + # + # You can set a command specific environment and cwd: + # + # run("echo 1", :cwd => "/tmp", :environment => {"FOO" => "BAR"}) + # + # This will run 'echo 1' from /tmp with FOO=BAR in addition to a setting forcing + # LC_ALL = C. To prevent LC_ALL from being set either set it specifically or: + # + # run("echo 1", :cwd => "/tmp", :environment => nil) + # + # Exceptions here will be handled by the usual agent exception handler or any + # specific one you create, if you dont it will just fall through and be sent + # to the client. + # + # If the shell handler fails to return a Process::Status instance for exit + # status this method will return -1 as the exit status + def run(command, options={}) + shellopts = {} + + # force stderr and stdout to be strings as the library + # will append data to them if given using the << method. + # + # if the data pased to :stderr or :stdin is a Symbol + # add that into the reply hash with that Symbol + [:stderr, :stdout].each do |k| + if options.include?(k) + if options[k].is_a?(Symbol) + reply[ options[k] ] = "" + shellopts[k] = reply[ options[k] ] + else + if options[k].respond_to?("<<") + shellopts[k] = options[k] + else + reply.fail! "#{k} should support << while calling run(#{command})" + end + end + end + end + + [:stdin, :cwd, :environment].each do |k| + if options.include?(k) + shellopts[k] = options[k] + end + end + + shell = Shell.new(command, shellopts) + + shell.runcommand + + if options[:chomp] + shellopts[:stdout].chomp! if shellopts[:stdout].is_a?(String) + shellopts[:stderr].chomp! if shellopts[:stderr].is_a?(String) + end + + shell.status.exitstatus rescue -1 + end + + # Registers meta data for the introspection hash + def self.metadata(data) + agent = File.basename(caller.first).split(":").first + + log_code(:PLMC34, "setting meta data in agents have been deprecated, DDL files are now being used for this information. Please update the '%{agent}' agent", :warn, :agent => agent) + end + + # Creates the needed activate? class in a manner similar to the other + # helpers like action, authorized_by etc + # + # activate_when do + # File.exist?("/usr/bin/puppet") + # end + def self.activate_when(&block) + (class << self; self; end).instance_eval do + define_method("activate?", &block) + end + end + + # Creates a new action with the block passed and sets some defaults + # + # action "status" do + # # logic here to restart service + # end + def self.action(name, &block) + raise "Need to pass a body for the action" unless block_given? + + self.module_eval { define_method("#{name}_action", &block) } + end + + # Helper that creates a method on the class that will call your authorization + # plugin. If your plugin raises an exception that will abort the request + def self.authorized_by(plugin) + plugin = plugin.to_s.capitalize + + # turns foo_bar into FooBar + plugin = plugin.to_s.split("_").map {|v| v.capitalize}.join + pluginname = "MCollective::Util::#{plugin}" + + PluginManager.loadclass(pluginname) unless MCollective::Util.constants.include?(plugin) + + class_eval(" + def authorization_hook(request) + #{pluginname}.authorize(request) + end + ") + end + + # Validates a data member, if validation is a regex then it will try to match it + # else it supports testing object types only: + # + # validate :msg, String + # validate :msg, /^[\w\s]+$/ + # + # There are also some special helper validators: + # + # validate :command, :shellsafe + # validate :command, :ipv6address + # validate :command, :ipv4address + # validate :command, :boolean + # validate :command, ["start", "stop"] + # + # It will raise appropriate exceptions that the RPC system understand + def validate(key, validation) + raise MissingRPCData, "please supply a #{key} argument" unless @request.include?(key) + + Validator.validate(@request[key], validation) + rescue ValidatorError => e + raise InvalidRPCData, "Input %s did not pass validation: %s" % [ key, e.message ] + end + + # convenience wrapper around Util#shellescape + def shellescape(str) + Util.shellescape(str) + end + + # handles external actions + def implemented_by(command, type=:json) + runner = ActionRunner.new(command, request, type) + + res = runner.run + + reply.fail! "Did not receive data from #{command}" unless res.include?(:data) + reply.fail! "Reply data from #{command} is not a Hash" unless res[:data].is_a?(Hash) + + reply.data.merge!(res[:data]) + + if res[:exitstatus] > 0 + reply.fail "Failed to run #{command}: #{res[:stderr]}", res[:exitstatus] + end + rescue Exception => e + Log.warn("Unhandled #{e.class} exception during #{request.agent}##{request.action}: #{e}") + reply.fail! "Unexpected failure calling #{command}: #{e.class}: #{e}" + end + + # Called at the end of the RPC::Agent standard initialize method + # use this to adjust meta parameters, timeouts and any setup you + # need to do. + # + # This will not be called right when the daemon starts up, we use + # lazy loading and initialization so it will only be called the first + # time a request for this agent arrives. + def startup_hook + end + + # Called just after a message was received from the middleware before + # it gets passed to the handlers. @request and @reply will already be + # set, the msg passed is the message as received from the normal + # mcollective runner and the connection is the actual connector. + def before_processing_hook(msg, connection) + end + + # Called at the end of processing just before the response gets sent + # to the middleware. + # + # This gets run outside of the main exception handling block of the agent + # so you should handle any exceptions you could raise yourself. The reason + # it is outside of the block is so you'll have access to even status codes + # set by the exception handlers. If you do raise an exception it will just + # be passed onto the runner and processing will fail. + def after_processing_hook + end + + # Gets called right after a request was received and calls audit plugins + # + # Agents can disable auditing by just overriding this method with a noop one + # this might be useful for agents that gets a lot of requests or simply if you + # do not care for the auditing in a specific agent. + def audit_request(msg, connection) + PluginManager["rpcaudit_plugin"].audit_request(msg, connection) if @config.rpcaudit + rescue Exception => e + logexception(:PLMC39, "Audit failed with an error, processing the request will continue.", :warn, e) + end + end + end +end diff --git a/lib/mcollective/rpc/audit.rb b/lib/mcollective/rpc/audit.rb new file mode 100644 index 0000000..7292422 --- /dev/null +++ b/lib/mcollective/rpc/audit.rb @@ -0,0 +1,38 @@ +module MCollective + module RPC + # Auditing of requests is done only for SimpleRPC requests, you provide + # a plugin in the MCollective::Audit::* namespace which the SimpleRPC + # framework calls for each message + # + # We provide a simple one that logs to a logfile in the class + # MCollective::Audit::Logfile you can create your own: + # + # Create a class in plugins/mcollective/audit/.rb + # + # You must inherit from MCollective::RPC::Audit which will take + # care of registering you with the plugin system. + # + # Your plugin must provide audit_request(request, connection) + # the request parameter will be an instance of MCollective::RPC::Request + # + # To enable auditing you should set: + # + # rpcaudit = 1 + # rpcauditprovider = Logfile + # + # in the config file this will enable logging using the + # MCollective::Audit::Logile class + # + # The Audit class acts as a base for audit plugins and takes care of registering them + # with the plugin manager + class Audit + def self.inherited(klass) + PluginManager << {:type => "rpcaudit_plugin", :class => klass.to_s} + end + + def audit_request(request, connection) + @log.error("audit_request is not implimented in #{this.class}") + end + end + end +end diff --git a/lib/mcollective/rpc/client.rb b/lib/mcollective/rpc/client.rb new file mode 100644 index 0000000..21efd74 --- /dev/null +++ b/lib/mcollective/rpc/client.rb @@ -0,0 +1,958 @@ +module MCollective + module RPC + # The main component of the Simple RPC client system, this wraps around MCollective::Client + # and just brings in a lot of convention and standard approached. + class Client + attr_accessor :timeout, :verbose, :filter, :config, :progress, :ttl, :reply_to + attr_reader :client, :stats, :ddl, :agent, :limit_targets, :limit_method, :output_format, :batch_size, :batch_sleep_time, :batch_mode + attr_reader :discovery_options, :discovery_method, :limit_seed + + @@initial_options = nil + + # Creates a stub for a remote agent, you can pass in an options array in the flags + # which will then be used else it will just create a default options array with + # filtering enabled based on the standard command line use. + # + # rpc = RPC::Client.new("rpctest", :configfile => "client.cfg", :options => options) + # + # You typically would not call this directly you'd use MCollective::RPC#rpcclient instead + # which is a wrapper around this that can be used as a Mixin + def initialize(agent, flags = {}) + if flags.include?(:options) + initial_options = flags[:options] + + elsif @@initial_options + initial_options = Marshal.load(@@initial_options) + + else + oparser = MCollective::Optionparser.new({:verbose => false, :progress_bar => true, :mcollective_limit_targets => false, :batch_size => nil, :batch_sleep_time => 1}, "filter") + + initial_options = oparser.parse do |parser, opts| + if block_given? + yield(parser, opts) + end + + Helpers.add_simplerpc_options(parser, opts) + end + + @@initial_options = Marshal.dump(initial_options) + end + + @initial_options = initial_options + + @config = initial_options[:config] + @client = MCollective::Client.new(@config) + @client.options = initial_options + + @stats = Stats.new + @agent = agent + @timeout = initial_options[:timeout] || 5 + @verbose = initial_options[:verbose] + @filter = initial_options[:filter] || Util.empty_filter + @discovered_agents = nil + @progress = initial_options[:progress_bar] + @limit_targets = initial_options[:mcollective_limit_targets] + @limit_method = Config.instance.rpclimitmethod + @limit_seed = initial_options[:limit_seed] || nil + @output_format = initial_options[:output_format] || :console + @force_direct_request = false + @reply_to = initial_options[:reply_to] + @discovery_method = initial_options[:discovery_method] || Config.instance.default_discovery_method + @discovery_options = initial_options[:discovery_options] || [] + @force_display_mode = initial_options[:force_display_mode] || false + + @batch_size = Integer(initial_options[:batch_size] || 0) + @batch_sleep_time = Float(initial_options[:batch_sleep_time] || 1) + @batch_mode = @batch_size > 0 + + agent_filter agent + + @discovery_timeout = @initial_options.fetch(:disctimeout, nil) + + @collective = @client.collective + @ttl = initial_options[:ttl] || Config.instance.ttl + + # if we can find a DDL for the service override + # the timeout of the client so we always magically + # wait appropriate amounts of time. + # + # We add the discovery timeout to the ddl supplied + # timeout as the discovery timeout tends to be tuned + # for local network conditions and fact source speed + # which would other wise not be accounted for and + # some results might get missed. + # + # We do this only if the timeout is the default 5 + # seconds, so that users cli overrides will still + # get applied + # + # DDLs are required, failure to find a DDL is fatal + @ddl = DDL.new(agent) + @stats.ddl = @ddl + @timeout = @ddl.meta[:timeout] + discovery_timeout if @timeout == 5 + + # allows stderr and stdout to be overridden for testing + # but also for web apps that might not want a bunch of stuff + # generated to actual file handles + if initial_options[:stderr] + @stderr = initial_options[:stderr] + else + @stderr = STDERR + @stderr.sync = true + end + + if initial_options[:stdout] + @stdout = initial_options[:stdout] + else + @stdout = STDOUT + @stdout.sync = true + end + end + + # Disconnects cleanly from the middleware + def disconnect + @client.disconnect + end + + # Returns help for an agent if a DDL was found + def help(template) + @ddl.help(template) + end + + # Creates a suitable request hash for the SimpleRPC agent. + # + # You'd use this if you ever wanted to take care of sending + # requests on your own - perhaps via Client#sendreq if you + # didn't care for responses. + # + # In that case you can just do: + # + # msg = your_rpc.new_request("some_action", :foo => :bar) + # filter = your_rpc.filter + # + # your_rpc.client.sendreq(msg, msg[:agent], filter) + # + # This will send a SimpleRPC request to the action some_action + # with arguments :foo = :bar, it will return immediately and + # you will have no indication at all if the request was receieved or not + # + # Clearly the use of this technique should be limited and done only + # if your code requires such a thing + def new_request(action, data) + callerid = PluginManager["security_plugin"].callerid + + raise 'callerid received from security plugin is not valid' unless PluginManager["security_plugin"].valid_callerid?(callerid) + + {:agent => @agent, + :action => action, + :caller => callerid, + :data => data} + end + + # For the provided arguments and action the input arguments get + # modified by supplying any defaults provided in the DDL for arguments + # that were not supplied in the request + # + # We then pass the modified arguments to the DDL for validation + def validate_request(action, args) + raise "No DDL found for agent %s cannot validate inputs" % @agent unless @ddl + + @ddl.set_default_input_arguments(action, args) + @ddl.validate_rpc_request(action, args) + end + + # Magic handler to invoke remote methods + # + # Once the stub is created using the constructor or the RPC#rpcclient helper you can + # call remote actions easily: + # + # ret = rpc.echo(:msg => "hello world") + # + # This will call the 'echo' action of the 'rpctest' agent and return the result as an array, + # the array will be a simplified result set from the usual full MCollective::Client#req with + # additional error codes and error text: + # + # { + # :sender => "remote.box.com", + # :statuscode => 0, + # :statusmsg => "OK", + # :data => "hello world" + # } + # + # If :statuscode is 0 then everything went find, if it's 1 then you supplied the correct arguments etc + # but the request could not be completed, you'll find a human parsable reason in :statusmsg then. + # + # Codes 2 to 5 maps directly to UnknownRPCAction, MissingRPCData, InvalidRPCData and UnknownRPCError + # see below for a description of those, in each case :statusmsg would be the reason for failure. + # + # To get access to the full result of the MCollective::Client#req calls you can pass in a block: + # + # rpc.echo(:msg => "hello world") do |resp| + # pp resp + # end + # + # In this case resp will the result from MCollective::Client#req. Instead of returning simple + # text and codes as above you'll also need to handle the following exceptions: + # + # UnknownRPCAction - There is no matching action on the agent + # MissingRPCData - You did not supply all the needed parameters for the action + # InvalidRPCData - The data you did supply did not pass validation + # UnknownRPCError - Some other error prevented the agent from running + # + # During calls a progress indicator will be shown of how many results we've received against + # how many nodes were discovered, you can disable this by setting progress to false: + # + # rpc.progress = false + # + # This supports a 2nd mode where it will send the SimpleRPC request and never handle the + # responses. It's a bit like UDP, it sends the request with the filter attached and you + # only get back the requestid, you have no indication about results. + # + # You can invoke this using: + # + # puts rpc.echo(:process_results => false) + # + # This will output just the request id. + # + # Batched processing is supported: + # + # printrpc rpc.ping(:batch_size => 5) + # + # This will do everything exactly as normal but communicate to only 5 + # agents at a time + def method_missing(method_name, *args, &block) + # set args to an empty hash if nothings given + args = args[0] + args = {} if args.nil? + + action = method_name.to_s + + @stats.reset + + validate_request(action, args) + + # if a global batch size is set just use that else set it + # in the case that it was passed as an argument + batch_mode = args.include?(:batch_size) || @batch_mode + batch_size = args.delete(:batch_size) || @batch_size + batch_sleep_time = args.delete(:batch_sleep_time) || @batch_sleep_time + + # if we were given a batch_size argument thats 0 and batch_mode was + # determined to be on via global options etc this will allow a batch_size + # of 0 to disable or batch_mode for this call only + batch_mode = (batch_mode && Integer(batch_size) > 0) + + # Handle single target requests by doing discovery and picking + # a random node. Then do a custom request specifying a filter + # that will only match the one node. + if @limit_targets + target_nodes = pick_nodes_from_discovered(@limit_targets) + Log.debug("Picked #{target_nodes.join(',')} as limited target(s)") + + custom_request(action, args, target_nodes, {"identity" => /^(#{target_nodes.join('|')})$/}, &block) + elsif batch_mode + call_agent_batched(action, args, options, batch_size, batch_sleep_time, &block) + else + call_agent(action, args, options, :auto, &block) + end + end + + # Constructs custom requests with custom filters and discovery data + # the idea is that this would be used in web applications where you + # might be using a cached copy of data provided by a registration agent + # to figure out on your own what nodes will be responding and what your + # filter would be. + # + # This will help you essentially short circuit the traditional cycle of: + # + # mc discover / call / wait for discovered nodes + # + # by doing discovery however you like, contructing a filter and a list of + # nodes you expect responses from. + # + # Other than that it will work exactly like a normal call, blocks will behave + # the same way, stats will be handled the same way etcetc + # + # If you just wanted to contact one machine for example with a client that + # already has other filter options setup you can do: + # + # puppet.custom_request("runonce", {}, ["your.box.com"], {:identity => "your.box.com"}) + # + # This will do runonce action on just 'your.box.com', no discovery will be + # done and after receiving just one response it will stop waiting for responses + # + # If direct_addressing is enabled in the config file you can provide an empty + # hash as a filter, this will force that request to be a directly addressed + # request which technically does not need filters. If you try to use this + # mode with direct addressing disabled an exception will be raise + def custom_request(action, args, expected_agents, filter = {}, &block) + validate_request(action, args) + + if filter == {} && !Config.instance.direct_addressing + raise "Attempted to do a filterless custom_request without direct_addressing enabled, preventing unexpected call to all nodes" + end + + @stats.reset + + custom_filter = Util.empty_filter + custom_options = options.clone + + # merge the supplied filter with the standard empty one + # we could just use the merge method but I want to be sure + # we dont merge in stuff that isnt actually valid + ["identity", "fact", "agent", "cf_class", "compound"].each do |ftype| + if filter.include?(ftype) + custom_filter[ftype] = [filter[ftype], custom_filter[ftype]].flatten + end + end + + # ensure that all filters at least restrict the call to the agent we're a proxy for + custom_filter["agent"] << @agent unless custom_filter["agent"].include?(@agent) + custom_options[:filter] = custom_filter + + # Fake out the stats discovery would have put there + @stats.discovered_agents([expected_agents].flatten) + + # Handle fire and forget requests + # + # If a specific reply-to was set then from the client perspective this should + # be a fire and forget request too since no response will ever reach us - it + # will go to the reply-to destination + if args[:process_results] == false || @reply_to + return fire_and_forget_request(action, args, custom_filter) + end + + # Now do a call pretty much exactly like in method_missing except with our own + # options and discovery magic + if block_given? + call_agent(action, args, custom_options, [expected_agents].flatten) do |r| + block.call(r) + end + else + call_agent(action, args, custom_options, [expected_agents].flatten) + end + end + + def discovery_timeout + return @discovery_timeout if @discovery_timeout + return @client.discoverer.ddl.meta[:timeout] + end + + def discovery_timeout=(timeout) + @discovery_timeout = Float(timeout) + + # we calculate the overall timeout from the DDL of the agent and + # the supplied discovery timeout unless someone specifically + # specifies a timeout to the constructor + # + # But if we also then specifically set a discovery_timeout on the + # agent that has to override the supplied timeout so we then + # calculate a correct timeout based on DDL timeout and the + # supplied discovery timeout + @timeout = @ddl.meta[:timeout] + discovery_timeout + end + + # Sets the discovery method. If we change the method there are a + # number of steps to take: + # + # - set the new method + # - if discovery options were provided, re-set those to initially + # provided ones else clear them as they might now apply to a + # different provider + # - update the client options so it knows there is a new discovery + # method in force + # - reset discovery data forcing a discover on the next request + # + # The remaining item is the discovery timeout, we leave that as is + # since that is the user supplied timeout either via initial options + # or via specifically setting it on the client. + def discovery_method=(method) + @discovery_method = method + + if @initial_options[:discovery_options] + @discovery_options = @initial_options[:discovery_options] + else + @discovery_options.clear + end + + @client.options = options + + reset + end + + def discovery_options=(options) + @discovery_options = [options].flatten + reset + end + + # Sets the class filter + def class_filter(klass) + @filter["cf_class"] << klass + @filter["cf_class"].compact! + reset + end + + # Sets the fact filter + def fact_filter(fact, value=nil, operator="=") + return if fact.nil? + return if fact == false + + if value.nil? + parsed = Util.parse_fact_string(fact) + @filter["fact"] << parsed unless parsed == false + else + parsed = Util.parse_fact_string("#{fact}#{operator}#{value}") + @filter["fact"] << parsed unless parsed == false + end + + @filter["fact"].compact! + reset + end + + # Sets the agent filter + def agent_filter(agent) + @filter["agent"] << agent + @filter["agent"].compact! + reset + end + + # Sets the identity filter + def identity_filter(identity) + @filter["identity"] << identity + @filter["identity"].compact! + reset + end + + # Set a compound filter + def compound_filter(filter) + @filter["compound"] << Matcher.create_compound_callstack(filter) + reset + end + + # Resets various internal parts of the class, most importantly it clears + # out the cached discovery + def reset + @discovered_agents = nil + end + + # Reet the filter to an empty one + def reset_filter + @filter = Util.empty_filter + agent_filter @agent + end + + # Does discovery based on the filters set, if a discovery was + # previously done return that else do a new discovery. + # + # Alternatively if identity filters are given and none of them are + # regular expressions then just use the provided data as discovered + # data, avoiding discovery + # + # Discovery can be forced if direct_addressing is enabled by passing + # in an array of nodes with :nodes or JSON data like those produced + # by mcollective RPC JSON output using :json + # + # Will show a message indicating its doing discovery if running + # verbose or if the :verbose flag is passed in. + # + # Use reset to force a new discovery + def discover(flags={}) + flags.keys.each do |key| + raise "Unknown option #{key} passed to discover" unless [:verbose, :hosts, :nodes, :json].include?(key) + end + + flags.include?(:verbose) ? verbose = flags[:verbose] : verbose = @verbose + + verbose = false unless @output_format == :console + + # flags[:nodes] and flags[:hosts] are the same thing, we should never have + # allowed :hosts as that was inconsistent with the established terminology + flags[:nodes] = flags.delete(:hosts) if flags.include?(:hosts) + + reset if flags[:nodes] || flags[:json] + + unless @discovered_agents + # if either hosts or JSON is supplied try to figure out discovery data from there + # if direct_addressing is not enabled this is a critical error as the user might + # not have supplied filters so raise an exception + if flags[:nodes] || flags[:json] + raise "Can only supply discovery data if direct_addressing is enabled" unless Config.instance.direct_addressing + + hosts = [] + + if flags[:nodes] + hosts = Helpers.extract_hosts_from_array(flags[:nodes]) + elsif flags[:json] + hosts = Helpers.extract_hosts_from_json(flags[:json]) + end + + raise "Could not find any hosts in discovery data provided" if hosts.empty? + + @discovered_agents = hosts + @force_direct_request = true + + # if an identity filter is supplied and it is all strings no regex we can use that + # as discovery data, technically the identity filter is then redundant if we are + # in direct addressing mode and we could empty it out but this use case should + # only really be for a few -I's on the CLI + # + # For safety we leave the filter in place for now, that way we can support this + # enhancement also in broadcast mode. + # + # This is only needed for the 'mc' discovery method, other methods might change + # the concept of identity to mean something else so we should pass the full + # identity filter to them + elsif options[:filter]["identity"].size > 0 && @discovery_method == "mc" + regex_filters = options[:filter]["identity"].select{|i| i.match("^\/")}.size + + if regex_filters == 0 + @discovered_agents = options[:filter]["identity"].clone + @force_direct_request = true if Config.instance.direct_addressing + end + end + end + + # All else fails we do it the hard way using a traditional broadcast + unless @discovered_agents + @stats.time_discovery :start + + @client.options = options + + # if compound filters are used the only real option is to use the mc + # discovery plugin since its the only capable of using data queries etc + # and we do not want to degrade that experience just to allow compounds + # on other discovery plugins the UX would be too bad raising complex sets + # of errors etc. + @client.discoverer.force_discovery_method_by_filter(options[:filter]) + + if verbose + actual_timeout = @client.discoverer.discovery_timeout(discovery_timeout, options[:filter]) + + if actual_timeout > 0 + @stderr.print("Discovering hosts using the %s method for %d second(s) .... " % [@client.discoverer.discovery_method, actual_timeout]) + else + @stderr.print("Discovering hosts using the %s method .... " % [@client.discoverer.discovery_method]) + end + end + + # if the requested limit is a pure number and not a percent + # and if we're configured to use the first found hosts as the + # limit method then pass in the limit thus minimizing the amount + # of work we do in the discover phase and speeding it up significantly + if @limit_method == :first and @limit_targets.is_a?(Fixnum) + @discovered_agents = @client.discover(@filter, discovery_timeout, @limit_targets) + else + @discovered_agents = @client.discover(@filter, discovery_timeout) + end + + @stderr.puts(@discovered_agents.size) if verbose + + @force_direct_request = @client.discoverer.force_direct_mode? + + @stats.time_discovery :end + end + + @stats.discovered_agents(@discovered_agents) + RPC.discovered(@discovered_agents) + + @discovered_agents + end + + # Provides a normal options hash like you would get from + # Optionparser + def options + {:disctimeout => discovery_timeout, + :timeout => @timeout, + :verbose => @verbose, + :filter => @filter, + :collective => @collective, + :output_format => @output_format, + :ttl => @ttl, + :discovery_method => @discovery_method, + :discovery_options => @discovery_options, + :force_display_mode => @force_display_mode, + :config => @config} + end + + # Sets the collective we are communicating with + def collective=(c) + raise "Unknown collective #{c}" unless Config.instance.collectives.include?(c) + + @collective = c + @client.options = options + reset + end + + # Sets and sanity checks the limit_targets variable + # used to restrict how many nodes we'll target + def limit_targets=(limit) + if limit.is_a?(String) + raise "Invalid limit specified: #{limit} valid limits are /^\d+%*$/" unless limit =~ /^\d+%*$/ + + begin + @limit_targets = Integer(limit) + rescue + @limit_targets = limit + end + else + @limit_targets = Integer(limit) + end + end + + # Sets and sanity check the limit_method variable + # used to determine how to limit targets if limit_targets is set + def limit_method=(method) + method = method.to_sym unless method.is_a?(Symbol) + + raise "Unknown limit method #{method} must be :random or :first" unless [:random, :first].include?(method) + + @limit_method = method + end + + # Sets the batch size, if the size is set to 0 that will disable batch mode + def batch_size=(limit) + raise "Can only set batch size if direct addressing is supported" unless Config.instance.direct_addressing + + @batch_size = Integer(limit) + @batch_mode = @batch_size > 0 + end + + def batch_sleep_time=(time) + raise "Can only set batch sleep time if direct addressing is supported" unless Config.instance.direct_addressing + + @batch_sleep_time = Float(time) + end + + # Pick a number of nodes from the discovered nodes + # + # The count should be a string that can be either + # just a number or a percentage like 10% + # + # It will select nodes from the discovered list based + # on the rpclimitmethod configuration option which can + # be either :first or anything else + # + # - :first would be a simple way to do a distance based + # selection + # - anything else will just pick one at random + # - if random chosen, and batch-seed set, then set srand + # for the generator, and reset afterwards + def pick_nodes_from_discovered(count) + if count =~ /%$/ + pct = Integer((discover.size * (count.to_f / 100))) + pct == 0 ? count = 1 : count = pct + else + count = Integer(count) + end + + return discover if discover.size <= count + + result = [] + + if @limit_method == :first + return discover[0, count] + else + # we delete from the discovered list because we want + # to be sure there is no chance that the same node will + # be randomly picked twice. So we have to clone the + # discovered list else this method will only ever work + # once per discovery cycle and not actually return the + # right nodes. + haystack = discover.clone + + if @limit_seed + haystack.sort! + srand(@limit_seed) + end + + count.times do + rnd = rand(haystack.size) + result << haystack.delete_at(rnd) + end + + # Reset random number generator to fresh seed + # As our seed from options is most likely short + srand if @limit_seed + end + + [result].flatten + end + + def load_aggregate_functions(action, ddl) + return nil unless ddl + return nil unless ddl.action_interface(action).keys.include?(:aggregate) + + return Aggregate.new(ddl.action_interface(action)) + + rescue => e + Log.error("Failed to load aggregate functions, calculating summaries disabled: %s: %s (%s)" % [e.backtrace.first, e.to_s, e.class]) + return nil + end + + def aggregate_reply(reply, aggregate) + return nil unless aggregate + + aggregate.call_functions(reply) + return aggregate + rescue Exception => e + Log.error("Failed to calculate aggregate summaries for reply from %s, calculating summaries disabled: %s: %s (%s)" % [reply[:senderid], e.backtrace.first, e.to_s, e.class]) + return nil + end + + def rpc_result_from_reply(agent, action, reply) + Result.new(agent, action, {:sender => reply[:senderid], :statuscode => reply[:body][:statuscode], + :statusmsg => reply[:body][:statusmsg], :data => reply[:body][:data]}) + end + + # for requests that do not care for results just + # return the request id and don't do any of the + # response processing. + # + # We send the :process_results flag with to the + # nodes so they can make decisions based on that. + # + # Should only be called via method_missing + def fire_and_forget_request(action, args, filter=nil) + validate_request(action, args) + + req = new_request(action.to_s, args) + + filter = options[:filter] unless filter + + message = Message.new(req, nil, {:agent => @agent, :type => :request, :collective => @collective, :filter => filter, :options => options}) + message.reply_to = @reply_to if @reply_to + + return @client.sendreq(message, nil) + end + + # Calls an agent in a way very similar to call_agent but it supports batching + # the queries to the network. + # + # The result sets, stats, block handling etc is all exactly like you would expect + # from normal call_agent. + # + # This is used by method_missing and works only with direct addressing mode + def call_agent_batched(action, args, opts, batch_size, sleep_time, &block) + raise "Batched requests requires direct addressing" unless Config.instance.direct_addressing + raise "Cannot bypass result processing for batched requests" if args[:process_results] == false + + batch_size = Integer(batch_size) + sleep_time = Float(sleep_time) + + Log.debug("Calling #{agent}##{action} in batches of #{batch_size} with sleep time of #{sleep_time}") + + @force_direct_request = true + + discovered = discover + results = [] + respcount = 0 + + if discovered.size > 0 + req = new_request(action.to_s, args) + + aggregate = load_aggregate_functions(action, @ddl) + + if @progress && !block_given? + twirl = Progress.new + @stdout.puts + @stdout.print twirl.twirl(respcount, discovered.size) + end + + @stats.requestid = nil + + discovered.in_groups_of(batch_size) do |hosts, last_batch| + message = Message.new(req, nil, {:agent => @agent, :type => :direct_request, :collective => @collective, :filter => opts[:filter], :options => opts}) + + # first time round we let the Message object create a request id + # we then re-use it for future requests to keep auditing sane etc + @stats.requestid = message.create_reqid unless @stats.requestid + message.requestid = @stats.requestid + + message.discovered_hosts = hosts.clone.compact + + @client.req(message) do |resp| + respcount += 1 + + if block_given? + aggregate = process_results_with_block(action, resp, block, aggregate) + else + @stdout.print twirl.twirl(respcount, discovered.size) if @progress + + result, aggregate = process_results_without_block(resp, action, aggregate) + + results << result + end + end + + @stats.noresponsefrom.concat @client.stats[:noresponsefrom] + @stats.responses += @client.stats[:responses] + @stats.blocktime += @client.stats[:blocktime] + sleep_time + @stats.totaltime += @client.stats[:totaltime] + @stats.discoverytime += @client.stats[:discoverytime] + + sleep sleep_time unless last_batch + end + + @stats.aggregate_summary = aggregate.summarize if aggregate + @stats.aggregate_failures = aggregate.failed if aggregate + else + @stderr.print("\nNo request sent, we did not discover any nodes.") + end + + @stats.finish_request + + RPC.stats(@stats) + + @stdout.print("\n") if @progress + + if block_given? + return stats + else + return [results].flatten + end + end + + # Handles traditional calls to the remote agents with full stats + # blocks, non blocks and everything else supported. + # + # Other methods of calling the nodes can reuse this code by + # for example specifying custom options and discovery data + def call_agent(action, args, opts, disc=:auto, &block) + # Handle fire and forget requests and make sure + # the :process_results value is set appropriately + # + # specific reply-to requests should be treated like + # fire and forget since the client will never get + # the responses + if args[:process_results] == false || @reply_to + return fire_and_forget_request(action, args) + else + args[:process_results] = true + end + + # Do discovery when no specific discovery array is given + # + # If an array is given set the force_direct_request hint that + # will tell the message object to be a direct request one + if disc == :auto + discovered = discover + else + @force_direct_request = true if Config.instance.direct_addressing + discovered = disc + end + + req = new_request(action.to_s, args) + + message = Message.new(req, nil, {:agent => @agent, :type => :request, :collective => @collective, :filter => opts[:filter], :options => opts}) + message.discovered_hosts = discovered.clone + + results = [] + respcount = 0 + + if discovered.size > 0 + message.type = :direct_request if @force_direct_request + + if @progress && !block_given? + twirl = Progress.new + @stdout.puts + @stdout.print twirl.twirl(respcount, discovered.size) + end + + aggregate = load_aggregate_functions(action, @ddl) + + @client.req(message) do |resp| + respcount += 1 + + if block_given? + aggregate = process_results_with_block(action, resp, block, aggregate) + else + @stdout.print twirl.twirl(respcount, discovered.size) if @progress + + result, aggregate = process_results_without_block(resp, action, aggregate) + + results << result + end + end + + @stats.aggregate_summary = aggregate.summarize if aggregate + @stats.aggregate_failures = aggregate.failed if aggregate + @stats.client_stats = @client.stats + else + @stderr.print("\nNo request sent, we did not discover any nodes.") + end + + @stats.finish_request + + RPC.stats(@stats) + + @stdout.print("\n\n") if @progress + + if block_given? + return stats + else + return [results].flatten + end + end + + # Handles result sets that has no block associated, sets fails and ok + # in the stats object and return a hash of the response to send to the + # caller + def process_results_without_block(resp, action, aggregate) + @stats.node_responded(resp[:senderid]) + + result = rpc_result_from_reply(@agent, action, resp) + aggregate = aggregate_reply(result, aggregate) if aggregate + + if resp[:body][:statuscode] == 0 || resp[:body][:statuscode] == 1 + @stats.ok if resp[:body][:statuscode] == 0 + @stats.fail if resp[:body][:statuscode] == 1 + else + @stats.fail + end + + [result, aggregate] + end + + # process client requests by calling a block on each result + # in this mode we do not do anything fancy with the result + # objects and we raise exceptions if there are problems with + # the data + def process_results_with_block(action, resp, block, aggregate) + @stats.node_responded(resp[:senderid]) + + result = rpc_result_from_reply(@agent, action, resp) + aggregate = aggregate_reply(result, aggregate) if aggregate + + if resp[:body][:statuscode] == 0 || resp[:body][:statuscode] == 1 + @stats.ok if resp[:body][:statuscode] == 0 + @stats.fail if resp[:body][:statuscode] == 1 + @stats.time_block_execution :start + + case block.arity + when 1 + block.call(resp) + when 2 + block.call(resp, result) + end + + @stats.time_block_execution :end + else + @stats.fail + + case resp[:body][:statuscode] + when 2 + raise UnknownRPCAction, resp[:body][:statusmsg] + when 3 + raise MissingRPCData, resp[:body][:statusmsg] + when 4 + raise InvalidRPCData, resp[:body][:statusmsg] + when 5 + raise UnknownRPCError, resp[:body][:statusmsg] + end + end + + return aggregate + end + end + end +end diff --git a/lib/mcollective/rpc/helpers.rb b/lib/mcollective/rpc/helpers.rb new file mode 100644 index 0000000..792736f --- /dev/null +++ b/lib/mcollective/rpc/helpers.rb @@ -0,0 +1,306 @@ +module MCollective + module RPC + # Various utilities for the RPC system + class Helpers + # Parse JSON output as produced by printrpc and extract + # the "sender" of each rpc response + # + # The simplist valid JSON based data would be: + # + # [ + # {"sender" => "example.com"}, + # {"sender" => "another.com"} + # ] + def self.extract_hosts_from_json(json) + hosts = JSON.parse(json) + + raise "JSON hosts list is not an array" unless hosts.is_a?(Array) + + hosts.map do |host| + raise "JSON host list is not an array of Hashes" unless host.is_a?(Hash) + raise "JSON host list does not have senders in it" unless host.include?("sender") + + host["sender"] + end.uniq + end + + # Given an array of something, make sure each is a string + # chomp off any new lines and return just the array of hosts + def self.extract_hosts_from_array(hosts) + [hosts].flatten.map do |host| + raise "#{host} should be a string" unless host.is_a?(String) + host.chomp + end + end + + # Returns a blob of text representing the results in a standard way + # + # It tries hard to do sane things so you often + # should not need to write your own display functions + # + # If the agent you are getting results for has a DDL + # it will use the hints in there to do the right thing specifically + # it will look at the values of display in the DDL to choose + # when to show results + # + # If you do not have a DDL you can pass these flags: + # + # printrpc exim.mailq, :flatten => true + # printrpc exim.mailq, :verbose => true + # + # If you've asked it to flatten the result it will not print sender + # hostnames, it will just print the result as if it's one huge result, + # handy for things like showing a combined mailq. + def self.rpcresults(result, flags = {}) + flags = {:verbose => false, :flatten => false, :format => :console, :force_display_mode => false}.merge(flags) + + result_text = "" + ddl = nil + + # if running in verbose mode, just use the old style print + # no need for all the DDL helpers obfuscating the result + if flags[:format] == :json + if STDOUT.tty? + result_text = JSON.pretty_generate(result) + else + result_text = result.to_json + end + else + if flags[:verbose] + result_text = old_rpcresults(result, flags) + else + [result].flatten.each do |r| + begin + ddl ||= DDL.new(r.agent).action_interface(r.action.to_s) + + sender = r[:sender] + status = r[:statuscode] + message = r[:statusmsg] + result = r[:data] + + if flags[:force_display_mode] + display = flags[:force_display_mode] + else + display = ddl[:display] + end + + # appand the results only according to what the DDL says + case display + when :ok + if status == 0 + result_text << text_for_result(sender, status, message, result, ddl) + end + + when :failed + if status > 0 + result_text << text_for_result(sender, status, message, result, ddl) + end + + when :always + result_text << text_for_result(sender, status, message, result, ddl) + + when :flatten + result_text << text_for_flattened_result(status, result) + + end + rescue Exception => e + # no DDL so just do the old style print unchanged for + # backward compat + result_text = old_rpcresults(result, flags) + end + end + end + end + + result_text + end + + # Return text representing a result + def self.text_for_result(sender, status, msg, result, ddl) + statusses = ["", + Util.colorize(:red, "Request Aborted"), + Util.colorize(:yellow, "Unknown Action"), + Util.colorize(:yellow, "Missing Request Data"), + Util.colorize(:yellow, "Invalid Request Data"), + Util.colorize(:red, "Unknown Request Status")] + + result_text = "%-40s %s\n" % [sender, statusses[status]] + result_text << " %s\n" % [Util.colorize(:yellow, msg)] unless msg == "OK" + + # only print good data, ignore data that results from failure + if status == 0 + if result.is_a?(Hash) + # figure out the lengths of the display as strings, we'll use + # it later to correctly justify the output + lengths = result.keys.map do |k| + begin + ddl[:output][k][:display_as].size + rescue + k.to_s.size + end + end + + result.keys.sort_by{|k| k}.each do |k| + # get all the output fields nicely lined up with a + # 3 space front padding + begin + display_as = ddl[:output][k][:display_as] + rescue + display_as = k.to_s + end + + display_length = display_as.size + padding = lengths.max - display_length + 3 + result_text << " " * padding + + result_text << "#{display_as}:" + + if [String, Numeric].include?(result[k].class) + lines = result[k].to_s.split("\n") + + if lines.empty? + result_text << "\n" + else + lines.each_with_index do |line, i| + i == 0 ? padtxt = " " : padtxt = " " * (padding + display_length + 2) + + result_text << "#{padtxt}#{line}\n" + end + end + else + padding = " " * (lengths.max + 5) + result_text << " " << result[k].pretty_inspect.split("\n").join("\n" << padding) << "\n" + end + end + elsif status == 1 + # for status 1 we dont want to show half baked + # data by default since the DDL will supply all the defaults + # it just doesnt look right + else + result_text << "\n\t" + result.pretty_inspect.split("\n").join("\n\t") + end + end + + result_text << "\n" + result_text + end + + # Returns text representing a flattened result of only good data + def self.text_for_flattened_result(status, result) + result_text = "" + + if status <= 1 + unless result.is_a?(String) + result_text << result.pretty_inspect + else + result_text << result + end + end + end + + # Backward compatible display block for results without a DDL + def self.old_rpcresults(result, flags = {}) + result_text = "" + + if flags[:flatten] + result.each do |r| + if r[:statuscode] <= 1 + data = r[:data] + + unless data.is_a?(String) + result_text << data.pretty_inspect + else + result_text << data + end + else + result_text << r.pretty_inspect + end + end + + result_text << "" + else + [result].flatten.each do |r| + + if flags[:verbose] + result_text << "%-40s: %s\n" % [r[:sender], r[:statusmsg]] + + if r[:statuscode] <= 1 + r[:data].pretty_inspect.split("\n").each {|m| result_text += " #{m}"} + result_text << "\n\n" + elsif r[:statuscode] == 2 + # dont print anything, no useful data to display + # past what was already shown + elsif r[:statuscode] == 3 + # dont print anything, no useful data to display + # past what was already shown + elsif r[:statuscode] == 4 + # dont print anything, no useful data to display + # past what was already shown + else + result_text << " #{r[:statusmsg]}" + end + else + unless r[:statuscode] == 0 + result_text << "%-40s %s\n" % [r[:sender], Util.colorize(:red, r[:statusmsg])] + end + end + end + end + + result_text << "" + end + + # Add SimpleRPC common options + def self.add_simplerpc_options(parser, options) + parser.separator "" + parser.separator "RPC Options" + + # add SimpleRPC specific options to all clients that use our library + parser.on('--np', '--no-progress', 'Do not show the progress bar') do |v| + options[:progress_bar] = false + end + + parser.on('--one', '-1', 'Send request to only one discovered nodes') do |v| + options[:mcollective_limit_targets] = 1 + end + + parser.on('--batch SIZE', Integer, 'Do requests in batches') do |v| + options[:batch_size] = v + end + + parser.on('--batch-sleep SECONDS', Float, 'Sleep time between batches') do |v| + options[:batch_sleep_time] = v + end + + parser.on('--limit-seed NUMBER', Integer, 'Seed value for deterministic random batching') do |v| + options[:limit_seed] = v + end + + parser.on('--limit-nodes COUNT', '--ln', '--limit', 'Send request to only a subset of nodes, can be a percentage') do |v| + raise "Invalid limit specified: #{v} valid limits are /^\d+%*$/" unless v =~ /^\d+%*$/ + + if v =~ /^\d+$/ + options[:mcollective_limit_targets] = v.to_i + else + options[:mcollective_limit_targets] = v + end + end + + parser.on('--json', '-j', 'Produce JSON output') do |v| + options[:progress_bar] = false + options[:output_format] = :json + end + + parser.on('--display MODE', 'Influence how results are displayed. One of ok, all or failed') do |v| + if v == "all" + options[:force_display_mode] = :always + else + options[:force_display_mode] = v.intern + end + + raise "--display has to be one of 'ok', 'all' or 'failed'" unless [:ok, :failed, :always].include?(options[:force_display_mode]) + end + end + end + end +end diff --git a/lib/mcollective/rpc/progress.rb b/lib/mcollective/rpc/progress.rb new file mode 100644 index 0000000..8c2f50b --- /dev/null +++ b/lib/mcollective/rpc/progress.rb @@ -0,0 +1,63 @@ +module MCollective + module RPC + # Class that shows a progress bar, currently only supports a twirling + # progress bar. + # + # You can specify a size for the progress bar if you want if you dont + # it will use the helper functions to figure out terminal dimensions + # and draw an appropriately sized bar + # + # p = Progress.new + # 100.times {|i| print p.twirl(i+1, 100) + "\r"};puts + # + # * [ ==================================================> ] 100 / 100 + class Progress + def initialize(size=nil) + @twirl = ['|', '/', '-', "\\", '|', '/', '-', "\\"] + @twirldex = 0 + + if size + @size = size + else + cols = Util.terminal_dimensions[0] - 22 + + # Defaults back to old behavior if it + # couldn't figure out the size or if + # its more than 60 wide + if cols <= 0 + @size = 0 + elsif cols > 60 + @size = 60 + else + @size = cols + end + end + end + + def twirl(current, total) + # if the size is negative there is just not enough + # space on the terminal, return a simpler version + return "\r#{current} / #{total}" if @size == 0 + + if current == total + txt = "\r %s [ " % Util.colorize(:green, "*") + else + txt = "\r %s [ " % Util.colorize(:red, @twirl[@twirldex]) + end + + dashes = ((current.to_f / total) * @size).round + + dashes.times { txt << "=" } + txt << ">" + + (@size - dashes).times { txt << " " } + + txt << " ] #{current} / #{total}" + + @twirldex == 7 ? @twirldex = 0 : @twirldex += 1 + + return txt + end + end + end +end diff --git a/lib/mcollective/rpc/reply.rb b/lib/mcollective/rpc/reply.rb new file mode 100644 index 0000000..574be55 --- /dev/null +++ b/lib/mcollective/rpc/reply.rb @@ -0,0 +1,85 @@ +module MCollective + module RPC + # Simple class to manage compliant replies to MCollective::RPC + class Reply + attr_accessor :statuscode, :statusmsg, :data + + def initialize(action, ddl) + @data = {} + @statuscode = 0 + @statusmsg = "OK" + @ddl = ddl + @action = action + + begin + initialize_data + rescue Exception => e + Log.warn("Could not pre-populate reply data from the DDL: %s: %s" % [e.class, e.to_s ]) + end + end + + def initialize_data + unless @ddl.actions.include?(@action) + raise "No action '%s' defined for agent '%s' in the DDL" % [@action, @ddl.pluginname] + end + + interface = @ddl.action_interface(@action) + + interface[:output].keys.each do |output| + @data[output] = interface[:output][output][:default] + end + end + + # Helper to fill in statusmsg and code on failure + def fail(msg, code=1) + @statusmsg = msg + @statuscode = code + end + + # Helper that fills in statusmsg and code but also raises an appropriate error + def fail!(msg, code=1) + @statusmsg = msg + @statuscode = code + + case code + when 1 + raise RPCAborted, msg + + when 2 + raise UnknownRPCAction, msg + + when 3 + raise MissingRPCData, msg + + when 4 + raise InvalidRPCData, msg + + else + raise UnknownRPCError, msg + end + end + + # Write to the data hash + def []=(key, val) + @data[key] = val + end + + # Read from the data hash + def [](key) + @data[key] + end + + def fetch(key, default) + @data.fetch(key, default) + end + + # Returns a compliant Hash of the reply that should be sent + # over the middleware + def to_hash + return {:statuscode => @statuscode, + :statusmsg => @statusmsg, + :data => @data} + end + end + end +end diff --git a/lib/mcollective/rpc/request.rb b/lib/mcollective/rpc/request.rb new file mode 100644 index 0000000..5243978 --- /dev/null +++ b/lib/mcollective/rpc/request.rb @@ -0,0 +1,62 @@ +module MCollective + module RPC + # Simple class to manage compliant requests for MCollective::RPC agents + class Request + attr_accessor :time, :action, :data, :sender, :agent, :uniqid, :caller, :ddl + + def initialize(msg, ddl) + @time = msg[:msgtime] + @action = msg[:body][:action] + @data = msg[:body][:data] + @sender = msg[:senderid] + @agent = msg[:body][:agent] + @uniqid = msg[:requestid] + @caller = msg[:callerid] || "unknown" + @ddl = ddl + end + + # If data is a hash, quick helper to get access to it's include? method + # else returns false + def include?(key) + return false unless @data.is_a?(Hash) + return @data.include?(key) + end + + # If no :process_results is specified always respond else respond + # based on the supplied property + def should_respond? + return @data[:process_results] if @data.include?(:process_results) + + return true + end + + # If data is a hash, gives easy access to its members, else returns nil + def [](key) + return nil unless @data.is_a?(Hash) + return @data[key] + end + + def fetch(key, default) + return nil unless @data.is_a?(Hash) + return @data.fetch(key, default) + end + + def to_hash + return {:agent => @agent, + :action => @action, + :data => @data} + end + + # Validate the request against the DDL + def validate! + @ddl.validate_rpc_request(@action, @data) + end + + def to_json + to_hash.merge!({:sender => @sender, + :callerid => @callerid, + :uniqid => @uniqid}).to_json + end + end + end +end diff --git a/lib/mcollective/rpc/result.rb b/lib/mcollective/rpc/result.rb new file mode 100644 index 0000000..291fd69 --- /dev/null +++ b/lib/mcollective/rpc/result.rb @@ -0,0 +1,45 @@ +module MCollective + module RPC + # Simple class to manage compliant results from MCollective::RPC agents + # + # Currently it just fakes Hash behaviour to the result to remain backward + # compatible but it also knows which agent and action produced it so you + # can associate results to a DDL + class Result + attr_reader :agent, :action, :results + + include Enumerable + + def initialize(agent, action, result={}) + @agent = agent + @action = action + @results = result + end + + def [](idx) + @results[idx] + end + + def []=(idx, item) + @results[idx] = item + end + + def fetch(key, default) + @results.fetch(key, default) + end + + def each + @results.each_pair {|k,v| yield(k,v) } + end + + def to_json(*a) + {:agent => @agent, + :action => @action, + :sender => @results[:sender], + :statuscode => @results[:statuscode], + :statusmsg => @results[:statusmsg], + :data => @results[:data]}.to_json(*a) + end + end + end +end diff --git a/lib/mcollective/rpc/stats.rb b/lib/mcollective/rpc/stats.rb new file mode 100644 index 0000000..ae909d2 --- /dev/null +++ b/lib/mcollective/rpc/stats.rb @@ -0,0 +1,256 @@ +module MCollective + module RPC + # Class to wrap all the stats and to keep track of some timings + class Stats + attr_accessor :noresponsefrom, :starttime, :discoverytime, :blocktime, :responses, :totaltime + attr_accessor :discovered, :discovered_nodes, :okcount, :failcount, :noresponsefrom, :responsesfrom + attr_accessor :requestid, :aggregate_summary, :ddl, :aggregate_failures + + def initialize + reset + end + + # Resets stats, if discovery time is set we keep it as it was + def reset + @noresponsefrom = [] + @responsesfrom = [] + @responses = 0 + @starttime = Time.now.to_f + @discoverytime = 0 unless @discoverytime + @blocktime = 0 + @totaltime = 0 + @discovered = 0 + @discovered_nodes = [] + @okcount = 0 + @failcount = 0 + @noresponsefrom = [] + @requestid = nil + @aggregate_summary = [] + @aggregate_failures = [] + end + + # returns a hash of our stats + def to_hash + {:noresponsefrom => @noresponsefrom, + :starttime => @starttime, + :discoverytime => @discoverytime, + :blocktime => @blocktime, + :responses => @responses, + :totaltime => @totaltime, + :discovered => @discovered, + :discovered_nodes => @discovered_nodes, + :noresponsefrom => @noresponsefrom, + :okcount => @okcount, + :requestid => @requestid, + :failcount => @failcount, + :aggregate_summary => @aggregate_summary, + :aggregate_failures => @aggregate_failures} + end + + # Fake hash access to keep things backward compatible + def [](key) + to_hash[key] + rescue + nil + end + + # increment the count of ok hosts + def ok + @okcount += 1 + rescue + @okcount = 1 + end + + # increment the count of failed hosts + def fail + @failcount += 1 + rescue + @failcount = 1 + end + + # Re-initializes the object with stats from the basic client + def client_stats=(stats) + @noresponsefrom = stats[:noresponsefrom] + @responses = stats[:responses] + @starttime = stats[:starttime] + @blocktime = stats[:blocktime] + @totaltime = stats[:totaltime] + @requestid = stats[:requestid] + @discoverytime = stats[:discoverytime] if @discoverytime == 0 + end + + # Utility to time discovery from :start to :end + def time_discovery(action) + if action == :start + @discovery_start = Time.now.to_f + elsif action == :end + @discoverytime = Time.now.to_f - @discovery_start + else + raise("Uknown discovery action #{action}") + end + rescue + @discoverytime = 0 + end + + # helper to time block execution time + def time_block_execution(action) + if action == :start + @block_start = Time.now.to_f + elsif action == :end + @blocktime += Time.now.to_f - @block_start + else + raise("Uknown block action #{action}") + end + rescue + @blocktime = 0 + end + + # Update discovered and discovered_nodes based on + # discovery results + def discovered_agents(agents) + @discovered_nodes = agents + @discovered = agents.size + end + + # Helper to calculate total time etc + def finish_request + @totaltime = @blocktime + @discoverytime + + # figures out who we had no responses from + dhosts = @discovered_nodes.clone + @responsesfrom.each {|r| dhosts.delete(r)} + @noresponsefrom = dhosts + rescue + @totaltime = 0 + @noresponsefrom = [] + end + + # Helper to keep track of who we received responses from + def node_responded(node) + @responsesfrom << node + rescue + @responsesfrom = [node] + end + + def text_for_aggregates + result = StringIO.new + + @aggregate_summary.each do |aggregate| + output_item = aggregate.result[:output] + + begin + action_interface = @ddl.action_interface(aggregate.action) + display_as = action_interface[:output][output_item][:display_as] + rescue + display_as = output_item + end + + if aggregate.is_a?(Aggregate::Result::Base) + aggregate_report = aggregate.to_s + else + next + end + + result.puts Util.colorize(:bold, "Summary of %s:" % display_as) + result.puts + unless aggregate_report == "" + result.puts aggregate.to_s.split("\n").map{|x| " " + x}.join("\n") + else + result.puts Util.colorize(:yellow, " No aggregate summary could be computed") + end + result.puts + end + + @aggregate_failures.each do |failed| + case(failed[:type]) + when :startup + message = "exception raised while processing startup hook" + when :create + message = "unspecified output '#{failed[:name]}' for the action" + when :process_result + message = "exception raised while processing result data" + when :summarize + message = "exception raised while summarizing" + end + + result.puts Util.colorize(:bold, "Summary of %s:" % failed[:name]) + result.puts + result.puts Util.colorize(:yellow, " Could not compute summary - %s" % message) + result.puts + end + + result.string + end + + # Returns a blob of text representing the request status based on the + # stats contained in this class + def report(caption = "rpc stats", summarize = true, verbose = false) + result_text = [] + + if verbose + if @aggregate_summary.size > 0 && summarize + result_text << text_for_aggregates + else + result_text << "" + end + + result_text << Util.colorize(:yellow, "---- #{caption} ----") + + if @discovered + @responses < @discovered ? color = :red : color = :reset + result_text << " Nodes: %s / %s" % [ Util.colorize(color, @discovered), Util.colorize(color, @responses) ] + else + result_text << " Nodes: #{@responses}" + end + + @failcount < 0 ? color = :red : color = :reset + + result_text << " Pass / Fail: %s / %s" % [Util.colorize(color, @okcount), Util.colorize(color, @failcount) ] + result_text << " Start Time: %s" % [Time.at(@starttime)] + result_text << " Discovery Time: %.2fms" % [@discoverytime * 1000] + result_text << " Agent Time: %.2fms" % [@blocktime * 1000] + result_text << " Total Time: %.2fms" % [@totaltime * 1000] + else + if @discovered + @responses < @discovered ? color = :red : color = :green + + if @aggregate_summary.size + @aggregate_failures.size > 0 && summarize + result_text << text_for_aggregates + else + result_text << "" + end + + result_text << "Finished processing %s / %s hosts in %.2f ms" % [Util.colorize(color, @responses), Util.colorize(color, @discovered), @blocktime * 1000] + else + result_text << "Finished processing %s hosts in %.2f ms" % [Util.colorize(:bold, @responses), @blocktime * 1000] + end + end + + if no_response_report != "" + result_text << "" << no_response_report + end + + result_text.join("\n") + end + + # Returns a blob of text indicating what nodes did not respond + def no_response_report + result_text = StringIO.new + + if @noresponsefrom.size > 0 + result_text.puts + result_text.puts Util.colorize(:red, "No response from:") + result_text.puts + + @noresponsefrom.sort.in_groups_of(3) do |c| + result_text.puts " %-30s%-30s%-30s" % c + end + + result_text.puts + end + + result_text.string + end + end + end +end diff --git a/lib/mcollective/runner.rb b/lib/mcollective/runner.rb new file mode 100644 index 0000000..19e6d1d --- /dev/null +++ b/lib/mcollective/runner.rb @@ -0,0 +1,137 @@ +module MCollective + # The main runner for the daemon, supports running in the foreground + # and the background, keeps detailed stats and provides hooks to access + # all this information + class Runner + include Translatable + + def initialize(configfile) + @config = Config.instance + @config.loadconfig(configfile) unless @config.configured + @config.mode = :server + + @stats = PluginManager["global_stats"] + + @security = PluginManager["security_plugin"] + @security.initiated_by = :node + + @connection = PluginManager["connector_plugin"] + @connection.connect + + @agents = Agents.new + + unless Util.windows? + Signal.trap("USR1") do + log_code(:PLMC2, "Reloading all agents after receiving USR1 signal", :info) + @agents.loadagents + end + + Signal.trap("USR2") do + log_code(:PLMC3, "Cycling logging level due to USR2 signal", :info) + + Log.cycle_level + end + else + Util.setup_windows_sleeper + end + end + + # Starts the main loop, before calling this you should initialize the MCollective::Config singleton. + def run + Data.load_data_sources + + Util.subscribe(Util.make_subscriptions("mcollective", :broadcast)) + Util.subscribe(Util.make_subscriptions("mcollective", :directed)) if @config.direct_addressing + + # Start the registration plugin if interval isn't 0 + begin + PluginManager["registration_plugin"].run(@connection) unless @config.registerinterval == 0 + rescue Exception => e + logexception(:PLMC4, "Failed to start registration plugin: %{error}", :error, e) + end + + loop do + begin + request = receive + + unless request.agent == "mcollective" + agentmsg(request) + else + log_code(:PLMC5, "Received a control message, possibly via 'mco controller' but this has been deprecated", :error) + end + rescue SignalException => e + logexception(:PLMC7, "Exiting after signal: %{error}", :warn, e) + @connection.disconnect + raise + + rescue MsgTTLExpired => e + logexception(:PLMC9, "Expired Message: %{error}", :warn, e) + + rescue NotTargettedAtUs => e + log_code(:PLMC6, "Message does not pass filters, ignoring", :debug) + + rescue Exception => e + logexception(:PLMC10, "Failed to handle message: %{error}", :warn, e, true) + end + end + end + + private + # Deals with messages directed to agents + def agentmsg(request) + log_code(:PLMC8, "Handling message for agent '%{agent}' on collective '%{collective} with requestid '%{requestid}'", :debug, :agent => request.agent, :collective => request.collective, :requestid => request.requestid) + + @agents.dispatch(request, @connection) do |reply_message| + reply(reply_message, request) if reply_message + end + end + + # Deals with messages sent to our control topic + def controlmsg(request) + Log.debug("Handling message for mcollectived controller") + + begin + case request.payload[:body] + when /^stats$/ + reply(@stats.to_hash, request) + + when /^reload_agent (.+)$/ + reply("reloaded #{$1} agent", request) if @agents.loadagent($1) + + when /^reload_agents$/ + + reply("reloaded all agents", request) if @agents.loadagents + + else + Log.error("Received an unknown message to the controller") + + end + rescue Exception => e + Log.error("Failed to handle control message: #{e}") + end + end + + # Receive a message from the connection handler + def receive + request = @connection.receive + request.type = :request + + @stats.received + + request.decode! + request.validate + + request + end + + # Sends a reply to a specific target topic + def reply(msg, request) + msg = Message.new(msg, nil, :request => request) + msg.encode! + msg.publish + + @stats.sent + end + end +end + diff --git a/lib/mcollective/runnerstats.rb b/lib/mcollective/runnerstats.rb new file mode 100644 index 0000000..337c969 --- /dev/null +++ b/lib/mcollective/runnerstats.rb @@ -0,0 +1,90 @@ +module MCollective + # Class to store stats about the mcollectived, it should live in the PluginManager + # so that agents etc can get hold of it and return the stats to callers + class RunnerStats + def initialize + @starttime = Time.now.to_i + @validated = 0 + @unvalidated = 0 + @filtered = 0 + @passed = 0 + @total = 0 + @replies = 0 + @ttlexpired = 0 + + @mutex = Mutex.new + end + + # Records a message that failed TTL checks + def ttlexpired + Log.debug("Incrementing ttl expired stat") + @ttlexpired += 1 + end + + # Records a message that passed the filters + def passed + Log.debug("Incrementing passed stat") + @passed += 1 + end + + # Records a message that didnt pass the filters + def filtered + Log.debug("Incrementing filtered stat") + @filtered += 1 + end + + # Records a message that validated ok + def validated + Log.debug("Incrementing validated stat") + @validated += 1 + end + + def unvalidated + Log.debug("Incrementing unvalidated stat") + @unvalidated += 1 + end + + # Records receipt of a message + def received + Log.debug("Incrementing total stat") + @total += 1 + end + + # Records sending a message + def sent + @mutex.synchronize do + Log.debug("Incrementing replies stat") + @replies += 1 + end + end + + # Returns a hash with all stats + def to_hash + stats = {:validated => @validated, + :unvalidated => @unvalidated, + :passed => @passed, + :filtered => @filtered, + :starttime => @starttime, + :total => @total, + :ttlexpired => @ttlexpired, + :replies => @replies} + + reply = {:stats => stats, + :threads => [], + :pid => Process.pid, + :times => {} } + + ::Process.times.each_pair{|k,v| + k = k.to_sym + reply[:times][k] = v + } + + Thread.list.each do |t| + reply[:threads] << "#{t.inspect}" + end + + reply[:agents] = Agents.agentlist + reply + end + end +end diff --git a/lib/mcollective/security.rb b/lib/mcollective/security.rb new file mode 100644 index 0000000..56d1cc4 --- /dev/null +++ b/lib/mcollective/security.rb @@ -0,0 +1,26 @@ +module MCollective + # Security is implimented using a module structure and installations + # can configure which module they want to use. + # + # Security modules deal with various aspects of authentication and authorization: + # + # - Determines if a filter excludes this host from dealing with a request + # - Serialization and Deserialization of messages + # - Validation of messages against keys, certificates or whatever the class choose to impliment + # - Encoding and Decoding of messages + # + # To impliment a new security class using SSL for example you would inherit from the base + # class and only impliment: + # + # - decodemsg + # - encodereply + # - encoderequest + # - validrequest? + # + # Each of these methods should increment various stats counters, see the default MCollective::Security::Psk module for examples of this + # + # Filtering can be extended by providing a new validate_filter? method. + module Security + autoload :Base, "mcollective/security/base" + end +end diff --git a/lib/mcollective/security/base.rb b/lib/mcollective/security/base.rb new file mode 100644 index 0000000..bcedf72 --- /dev/null +++ b/lib/mcollective/security/base.rb @@ -0,0 +1,244 @@ +module MCollective + module Security + # This is a base class the other security modules should inherit from + # it handles statistics and validation of messages that should in most + # cases apply to all security models. + # + # To create your own security plugin you should provide a plugin that inherits + # from this and provides the following methods: + # + # decodemsg - Decodes a message that was received from the middleware + # encodereply - Encodes a reply message to a previous request message + # encoderequest - Encodes a new request message + # validrequest? - Validates a request received from the middleware + # + # Optionally if you are identifying users by some other means like certificate name + # you can provide your own callerid method that can provide the rest of the system + # with an id, and you would see this id being usable in SimpleRPC authorization methods + # + # The @initiated_by variable will be set to either :client or :node depending on + # who is using this plugin. This is to help security providers that operate in an + # asymetric mode like public/private key based systems. + # + # Specifics of each of these are a bit fluid and the interfaces for this is not + # set in stone yet, specifically the encode methods will be provided with a helper + # that takes care of encoding the core requirements. The best place to see how security + # works is by looking at the provided MCollective::Security::PSK plugin. + class Base + attr_reader :stats + attr_accessor :initiated_by + + # Register plugins that inherits base + def self.inherited(klass) + PluginManager << {:type => "security_plugin", :class => klass.to_s} + end + + # Initializes configuration and logging as well as prepare a zero'd hash of stats + # various security methods and filter validators should increment stats, see MCollective::Security::Psk for a sample + def initialize + @config = Config.instance + @log = Log + @stats = PluginManager["global_stats"] + end + + # Takes a Hash with a filter in it and validates it against host information. + # + # At present this supports filter matches against the following criteria: + # + # - puppet_class|cf_class - Presence of a configuration management class in + # the file configured with classesfile + # - agent - Presence of a MCollective agent with a supplied name + # - fact - The value of a fact avout this system + # - identity - the configured identity of the system + # + # TODO: Support REGEX and/or multiple filter keys to be AND'd + def validate_filter?(filter) + failed = 0 + passed = 0 + + passed = 1 if Util.empty_filter?(filter) + + filter.keys.each do |key| + case key + when /puppet_class|cf_class/ + filter[key].each do |f| + Log.debug("Checking for class #{f}") + if Util.has_cf_class?(f) then + Log.debug("Passing based on configuration management class #{f}") + passed += 1 + else + Log.debug("Failing based on configuration management class #{f}") + failed += 1 + end + end + + when "compound" + filter[key].each do |compound| + result = false + truth_values = [] + + begin + compound.each do |expression| + case expression.keys.first + when "statement" + truth_values << Matcher.eval_compound_statement(expression).to_s + when "fstatement" + truth_values << Matcher.eval_compound_fstatement(expression.values.first) + when "and" + truth_values << "&&" + when "or" + truth_values << "||" + when "(" + truth_values << "(" + when ")" + truth_values << ")" + when "not" + truth_values << "!" + end + end + + result = eval(truth_values.join(" ")) + rescue DDLValidationError + result = false + end + + if result + Log.debug("Passing based on class and fact composition") + passed +=1 + else + Log.debug("Failing based on class and fact composition") + failed +=1 + end + end + + when "agent" + filter[key].each do |f| + if Util.has_agent?(f) || f == "mcollective" + Log.debug("Passing based on agent #{f}") + passed += 1 + else + Log.debug("Failing based on agent #{f}") + failed += 1 + end + end + + when "fact" + filter[key].each do |f| + if Util.has_fact?(f[:fact], f[:value], f[:operator]) + Log.debug("Passing based on fact #{f[:fact]} #{f[:operator]} #{f[:value]}") + passed += 1 + else + Log.debug("Failing based on fact #{f[:fact]} #{f[:operator]} #{f[:value]}") + failed += 1 + end + end + + when "identity" + unless filter[key].empty? + # Identity filters should not be 'and' but 'or' as each node can only have one identity + matched = filter[key].select{|f| Util.has_identity?(f)}.size + + if matched == 1 + Log.debug("Passing based on identity") + passed += 1 + else + Log.debug("Failed based on identity") + failed += 1 + end + end + end + end + + if failed == 0 && passed > 0 + Log.debug("Message passed the filter checks") + + @stats.passed + + return true + else + Log.debug("Message failed the filter checks") + + @stats.filtered + + return false + end + end + + def create_reply(reqid, agent, body) + Log.debug("Encoded a message for request #{reqid}") + + {:senderid => @config.identity, + :requestid => reqid, + :senderagent => agent, + :msgtime => Time.now.utc.to_i, + :body => body} + end + + def create_request(reqid, filter, msg, initiated_by, target_agent, target_collective, ttl=60) + Log.debug("Encoding a request for agent '#{target_agent}' in collective #{target_collective} with request id #{reqid}") + + {:body => msg, + :senderid => @config.identity, + :requestid => reqid, + :filter => filter, + :collective => target_collective, + :agent => target_agent, + :callerid => callerid, + :ttl => ttl, + :msgtime => Time.now.utc.to_i} + end + + # Give a MC::Message instance and a message id this will figure out if you the incoming + # message id matches the one the Message object is expecting and raise if its not + # + # Mostly used by security plugins to figure out if they should do the hard work of decrypting + # etc messages that would only later on be ignored + def should_process_msg?(msg, msgid) + if msg.expected_msgid + unless msg.expected_msgid == msgid + msgtext = "Got a message with id %s but was expecting %s, ignoring message" % [msgid, msg.expected_msgid] + Log.debug msgtext + raise MsgDoesNotMatchRequestID, msgtext + end + end + + true + end + + # Validates a callerid. We do not want to allow things like \ and / in + # callerids since other plugins make assumptions that these are safe strings. + # + # callerids are generally in the form uid=123 or cert=foo etc so we do that + # here but security plugins could override this for some complex uses + def valid_callerid?(id) + !!id.match(/^[\w]+=[\w\.\-]+$/) + end + + # Returns a unique id for the caller, by default we just use the unix + # user id, security plugins can provide their own means of doing ids. + def callerid + "uid=#{Process.uid}" + end + + # Security providers should provide this, see MCollective::Security::Psk + def validrequest?(req) + Log.error("validrequest? is not implemented in #{self.class}") + end + + # Security providers should provide this, see MCollective::Security::Psk + def encoderequest(sender, msg, filter={}) + Log.error("encoderequest is not implemented in #{self.class}") + end + + # Security providers should provide this, see MCollective::Security::Psk + def encodereply(sender, msg, requestcallerid=nil) + Log.error("encodereply is not implemented in #{self.class}") + end + + # Security providers should provide this, see MCollective::Security::Psk + def decodemsg(msg) + Log.error("decodemsg is not implemented in #{self.class}") + end + end + end +end diff --git a/lib/mcollective/shell.rb b/lib/mcollective/shell.rb new file mode 100644 index 0000000..257fc4b --- /dev/null +++ b/lib/mcollective/shell.rb @@ -0,0 +1,87 @@ +module MCollective + # Wrapper around systemu that handles executing of system commands + # in a way that makes stdout, stderr and status available. Supports + # timeouts and sets a default sane environment. + # + # s = Shell.new("date", opts) + # s.runcommand + # puts s.stdout + # puts s.stderr + # puts s.status.exitstatus + # + # Options hash can have: + # + # cwd - the working directory the command will be run from + # stdin - a string that will be sent to stdin of the program + # stdout - a variable that will receive stdout, must support << + # stderr - a variable that will receive stdin, must support << + # environment - the shell environment, defaults to include LC_ALL=C + # set to nil to clear the environment even of LC_ALL + # + class Shell + attr_reader :environment, :command, :status, :stdout, :stderr, :stdin, :cwd + + def initialize(command, options={}) + @environment = {"LC_ALL" => "C"} + @command = command + @status = nil + @stdout = "" + @stderr = "" + @stdin = nil + @cwd = Dir.tmpdir + + options.each do |opt, val| + case opt.to_s + when "stdout" + raise "stdout should support <<" unless val.respond_to?("<<") + @stdout = val + + when "stderr" + raise "stderr should support <<" unless val.respond_to?("<<") + @stderr = val + + when "stdin" + raise "stdin should be a String" unless val.is_a?(String) + @stdin = val + + when "cwd" + raise "Directory #{val} does not exist" unless File.directory?(val) + @cwd = val + + when "environment" + if val.nil? + @environment = {} + else + @environment.merge!(val.dup) + end + end + end + end + + # Actually does the systemu call passing in the correct environment, stdout and stderr + def runcommand + opts = {"env" => @environment, + "stdout" => @stdout, + "stderr" => @stderr, + "cwd" => @cwd} + + opts["stdin"] = @stdin if @stdin + + # Running waitpid on the cid here will start a thread + # with the waitpid in it, this way even if the thread + # that started this process gets killed due to agent + # timeout or such there will still be a waitpid waiting + # for the child to exit and not leave zombies. + @status = systemu(@command, opts) do |cid| + begin + sleep 1 + Process::waitpid(cid) + rescue SystemExit + rescue Errno::ECHILD + rescue Exception => e + Log.info("Unexpected exception received while waiting for child process: #{e.class}: #{e}") + end + end + end + end +end diff --git a/lib/mcollective/ssl.rb b/lib/mcollective/ssl.rb new file mode 100644 index 0000000..0c3abb9 --- /dev/null +++ b/lib/mcollective/ssl.rb @@ -0,0 +1,280 @@ +require 'openssl' +require 'base64' +require 'digest/sha1' + +module MCollective + # A class that assists in encrypting and decrypting data using a + # combination of RSA and AES + # + # Data will be AES encrypted for speed, the Key used in # the AES + # stage will be encrypted using RSA + # + # ssl = SSL.new(public_key, private_key, passphrase) + # + # data = File.read("largefile.dat") + # + # crypted_data = ssl.encrypt_with_private(data) + # + # pp crypted_data + # + # This will result in a hash of data like: + # + # crypted = {:key => "crd4NHvG....=", + # :data => "XWXlqN+i...=="} + # + # The key and data will all be base 64 encoded already by default + # you can pass a 2nd parameter as false to encrypt_with_private and + # counterparts that will prevent the base 64 encoding + # + # You can pass the data hash into ssl.decrypt_with_public which + # should return your original data + # + # There are matching methods for using a public key to encrypt + # data to be decrypted using a private key + class SSL + attr_reader :public_key_file, :private_key_file, :ssl_cipher + + def initialize(pubkey=nil, privkey=nil, passphrase=nil, cipher=nil) + @public_key_file = pubkey + @private_key_file = privkey + + @public_key = read_key(:public, pubkey) + @private_key = read_key(:private, privkey, passphrase) + + @ssl_cipher = "aes-256-cbc" + @ssl_cipher = Config.instance.ssl_cipher if Config.instance.ssl_cipher + @ssl_cipher = cipher if cipher + + raise "The supplied cipher '#{@ssl_cipher}' is not supported" unless OpenSSL::Cipher.ciphers.include?(@ssl_cipher) + end + + # Encrypts supplied data using AES and then encrypts using RSA + # the key and IV + # + # Return a hash with everything optionally base 64 encoded + def encrypt_with_public(plain_text, base64=true) + crypted = aes_encrypt(plain_text) + + if base64 + key = base64_encode(rsa_encrypt_with_public(crypted[:key])) + data = base64_encode(crypted[:data]) + else + key = rsa_encrypt_with_public(crypted[:key]) + data = crypted[:data] + end + + {:key => key, :data => data} + end + + # Encrypts supplied data using AES and then encrypts using RSA + # the key and IV + # + # Return a hash with everything optionally base 64 encoded + def encrypt_with_private(plain_text, base64=true) + crypted = aes_encrypt(plain_text) + + if base64 + key = base64_encode(rsa_encrypt_with_private(crypted[:key])) + data = base64_encode(crypted[:data]) + else + key = rsa_encrypt_with_private(crypted[:key]) + data = crypted[:data] + end + + {:key => key, :data => data} + end + + # Decrypts data, expects a hash as create with crypt_with_public + def decrypt_with_private(crypted, base64=true) + raise "Crypted data should include a key" unless crypted.include?(:key) + raise "Crypted data should include data" unless crypted.include?(:data) + + if base64 + key = rsa_decrypt_with_private(base64_decode(crypted[:key])) + aes_decrypt(key, base64_decode(crypted[:data])) + else + key = rsa_decrypt_with_private(crypted[:key]) + aes_decrypt(key, crypted[:data]) + end + end + + # Decrypts data, expects a hash as create with crypt_with_private + def decrypt_with_public(crypted, base64=true) + raise "Crypted data should include a key" unless crypted.include?(:key) + raise "Crypted data should include data" unless crypted.include?(:data) + + if base64 + key = rsa_decrypt_with_public(base64_decode(crypted[:key])) + aes_decrypt(key, base64_decode(crypted[:data])) + else + key = rsa_decrypt_with_public(crypted[:key]) + aes_decrypt(key, crypted[:data]) + end + end + + # Use the public key to RSA encrypt data + def rsa_encrypt_with_public(plain_string) + raise "No public key set" unless @public_key + + @public_key.public_encrypt(plain_string) + end + + # Use the private key to RSA decrypt data + def rsa_decrypt_with_private(crypt_string) + raise "No private key set" unless @private_key + + @private_key.private_decrypt(crypt_string) + end + + # Use the private key to RSA encrypt data + def rsa_encrypt_with_private(plain_string) + raise "No private key set" unless @private_key + + @private_key.private_encrypt(plain_string) + end + + # Use the public key to RSA decrypt data + def rsa_decrypt_with_public(crypt_string) + raise "No public key set" unless @public_key + + @public_key.public_decrypt(crypt_string) + end + + # encrypts a string, returns a hash of key, iv and data + def aes_encrypt(plain_string) + cipher = OpenSSL::Cipher::Cipher.new(ssl_cipher) + cipher.encrypt + + key = cipher.random_key + + cipher.key = key + cipher.pkcs5_keyivgen(key) + encrypted_data = cipher.update(plain_string) + cipher.final + + {:key => key, :data => encrypted_data} + end + + # decrypts a string given key, iv and data + def aes_decrypt(key, crypt_string) + cipher = OpenSSL::Cipher::Cipher.new(ssl_cipher) + + cipher.decrypt + cipher.key = key + cipher.pkcs5_keyivgen(key) + decrypted_data = cipher.update(crypt_string) + cipher.final + end + + # Signs a string using the private key + def sign(string, base64=false) + sig = @private_key.sign(OpenSSL::Digest::SHA1.new, string) + + base64 ? base64_encode(sig) : sig + end + + # Using the public key verifies that a string was signed using the private key + def verify_signature(signature, string, base64=false) + signature = base64_decode(signature) if base64 + + @public_key.verify(OpenSSL::Digest::SHA1.new, signature, string) + end + + # base 64 encode a string + def base64_encode(string) + SSL.base64_encode(string) + end + + def self.base64_encode(string) + Base64.encode64(string) + end + + # base 64 decode a string + def base64_decode(string) + SSL.base64_decode(string) + end + + def self.base64_decode(string) + Base64.decode64(string) + end + + def md5(string) + SSL.md5(string) + end + + def self.md5(string) + Digest::MD5.hexdigest(string) + end + + # Creates a RFC 4122 version 5 UUID. If string is supplied it will produce repeatable + # UUIDs for that string else a random 128bit string will be used from OpenSSL::BN + # + # Code used with permission from: + # https://github.com/kwilczynski/puppet-functions/blob/master/lib/puppet/parser/functions/uuid.rb + # + def self.uuid(string=nil) + string ||= OpenSSL::Random.random_bytes(16).unpack('H*').shift + + uuid_name_space_dns = "\x6b\xa7\xb8\x10\x9d\xad\x11\xd1\x80\xb4\x00\xc0\x4f\xd4\x30\xc8" + + sha1 = Digest::SHA1.new + sha1.update(uuid_name_space_dns) + sha1.update(string) + + # first 16 bytes.. + bytes = sha1.digest[0, 16].bytes.to_a + + # version 5 adjustments + bytes[6] &= 0x0f + bytes[6] |= 0x50 + + # variant is DCE 1.1 + bytes[8] &= 0x3f + bytes[8] |= 0x80 + + bytes = [4, 2, 2, 2, 6].collect do |i| + bytes.slice!(0, i).pack('C*').unpack('H*') + end + + bytes.join('-') + end + + # Reads either a :public or :private key from disk, uses an + # optional passphrase to read the private key + def read_key(type, key=nil, passphrase=nil) + return key if key.nil? + + raise "Could not find key #{key}" unless File.exist?(key) + raise "#{type} key file '#{key}' is empty" if File.zero?(key) + + if type == :public + begin + key = OpenSSL::PKey::RSA.new(File.read(key)) + rescue OpenSSL::PKey::RSAError + key = OpenSSL::X509::Certificate.new(File.read(key)).public_key + end + + # Ruby < 1.9.3 had a bug where it does not correctly clear the + # queue of errors while reading a key. It tries various ways + # to read the key and each failing attempt pushes an error onto + # the queue. With pubkeys only the 3rd attempt pass leaving 2 + # stale errors on the error queue. + # + # In 1.9.3 they fixed this by simply discarding the errors after + # every attempt. So we simulate this fix here for older rubies + # as without it we get SSL_read errors from the Stomp+TLS sessions + # + # We do this only on 1.8 relying on 1.9.3 to do the right thing + # and we do not support 1.9 less than 1.9.3 + # + # See http://bugs.ruby-lang.org/issues/4550 + OpenSSL.errors if Util.ruby_version =~ /^1.8/ + + return key + elsif type == :private + return OpenSSL::PKey::RSA.new(File.read(key), passphrase) + else + raise "Can only load :public or :private keys" + end + end + + end +end diff --git a/lib/mcollective/translatable.rb b/lib/mcollective/translatable.rb new file mode 100644 index 0000000..1d75ae3 --- /dev/null +++ b/lib/mcollective/translatable.rb @@ -0,0 +1,24 @@ +module MCollective + module Translatable + def t(msgid, default, args={}) + Util.t(msgid, {:default => default}.merge(args)) + end + + def log_code(msgid, default, level, args={}) + msg = "%s: %s" % [msgid, Util.t(msgid, {:default => default}.merge(args))] + + Log.log(level, msg, File.basename(caller[1])) + end + + def raise_code(msgid, default, level, args={}) + exception = CodedError.new(msgid, default, level, args) + exception.set_backtrace caller + + raise exception + end + + def logexception(msgid, default, level, e, backtrace=false) + Log.logexception(msgid, level, e, backtrace) + end + end +end diff --git a/lib/mcollective/unix_daemon.rb b/lib/mcollective/unix_daemon.rb new file mode 100644 index 0000000..dfacdfb --- /dev/null +++ b/lib/mcollective/unix_daemon.rb @@ -0,0 +1,37 @@ +module MCollective + class UnixDaemon + # Daemonize the current process + def self.daemonize + fork do + Process.setsid + exit if fork + Dir.chdir('/tmp') + STDIN.reopen('/dev/null') + STDOUT.reopen('/dev/null', 'a') + STDERR.reopen('/dev/null', 'a') + + yield + end + end + + def self.daemonize_runner(pid=nil) + raise "The Unix Daemonizer can not be used on the Windows Platform" if Util.windows? + + UnixDaemon.daemonize do + if pid + begin + File.open(pid, 'w') {|f| f.write(Process.pid) } + rescue Exception => e + end + end + + begin + runner = Runner.new(nil) + runner.run + ensure + File.unlink(pid) if pid && File.exist?(pid) + end + end + end + end +end diff --git a/lib/mcollective/util.rb b/lib/mcollective/util.rb new file mode 100644 index 0000000..9095cc3 --- /dev/null +++ b/lib/mcollective/util.rb @@ -0,0 +1,466 @@ +module MCollective + # Some basic utility helper methods useful to clients, agents, runner etc. + module Util + # Finds out if this MCollective has an agent by the name passed + # + # If the passed name starts with a / it's assumed to be regex + # and will use regex to match + def self.has_agent?(agent) + agent = Regexp.new(agent.gsub("\/", "")) if agent.match("^/") + + if agent.is_a?(Regexp) + if Agents.agentlist.grep(agent).size > 0 + return true + else + return false + end + else + return Agents.agentlist.include?(agent) + end + + false + end + + # On windows ^c can't interrupt the VM if its blocking on + # IO, so this sets up a dummy thread that sleeps and this + # will have the end result of being interruptable at least + # once a second. This is a common pattern found in Rails etc + def self.setup_windows_sleeper + Thread.new { loop { sleep 1 } } if Util.windows? + end + + # Checks if this node has a configuration management class by parsing the + # a text file with just a list of classes, recipes, roles etc. This is + # ala the classes.txt from puppet. + # + # If the passed name starts with a / it's assumed to be regex + # and will use regex to match + def self.has_cf_class?(klass) + klass = Regexp.new(klass.gsub("\/", "")) if klass.match("^/") + cfile = Config.instance.classesfile + + Log.debug("Looking for configuration management classes in #{cfile}") + + begin + File.readlines(cfile).each do |k| + if klass.is_a?(Regexp) + return true if k.chomp.match(klass) + else + return true if k.chomp == klass + end + end + rescue Exception => e + Log.warn("Parsing classes file '#{cfile}' failed: #{e.class}: #{e}") + end + + false + end + + # Gets the value of a specific fact, mostly just a duplicate of MCollective::Facts.get_fact + # but it kind of goes with the other classes here + def self.get_fact(fact) + Facts.get_fact(fact) + end + + # Compares fact == value, + # + # If the passed value starts with a / it's assumed to be regex + # and will use regex to match + def self.has_fact?(fact, value, operator) + + Log.debug("Comparing #{fact} #{operator} #{value}") + Log.debug("where :fact = '#{fact}', :operator = '#{operator}', :value = '#{value}'") + + fact = Facts[fact] + return false if fact.nil? + + fact = fact.clone + + if operator == '=~' + # to maintain backward compat we send the value + # as /.../ which is what 1.0.x needed. this strips + # off the /'s wich is what we need here + if value =~ /^\/(.+)\/$/ + value = $1 + end + + return true if fact.match(Regexp.new(value)) + + elsif operator == "==" + return true if fact == value + + elsif ['<=', '>=', '<', '>', '!='].include?(operator) + # Yuk - need to type cast, but to_i and to_f are overzealous + if value =~ /^[0-9]+$/ && fact =~ /^[0-9]+$/ + fact = Integer(fact) + value = Integer(value) + elsif value =~ /^[0-9]+.[0-9]+$/ && fact =~ /^[0-9]+.[0-9]+$/ + fact = Float(fact) + value = Float(value) + end + + return true if eval("fact #{operator} value") + end + + false + end + + # Checks if the configured identity matches the one supplied + # + # If the passed name starts with a / it's assumed to be regex + # and will use regex to match + def self.has_identity?(identity) + identity = Regexp.new(identity.gsub("\/", "")) if identity.match("^/") + + if identity.is_a?(Regexp) + return Config.instance.identity.match(identity) + else + return true if Config.instance.identity == identity + end + + false + end + + # Checks if the passed in filter is an empty one + def self.empty_filter?(filter) + filter == empty_filter || filter == {} + end + + # Creates an empty filter + def self.empty_filter + {"fact" => [], + "cf_class" => [], + "agent" => [], + "identity" => [], + "compound" => []} + end + + # Picks a config file defaults to ~/.mcollective + # else /etc/mcollective/client.cfg + def self.config_file_for_user + # expand_path is pretty lame, it relies on HOME environment + # which isnt't always there so just handling all exceptions + # here as cant find reverting to default + begin + config = File.expand_path("~/.mcollective") + + unless File.readable?(config) && File.file?(config) + config = "/etc/mcollective/client.cfg" + end + rescue Exception => e + config = "/etc/mcollective/client.cfg" + end + + return config + end + + # Creates a standard options hash + def self.default_options + {:verbose => false, + :disctimeout => nil, + :timeout => 5, + :config => config_file_for_user, + :collective => nil, + :discovery_method => nil, + :discovery_options => Config.instance.default_discovery_options, + :filter => empty_filter} + end + + def self.make_subscriptions(agent, type, collective=nil) + config = Config.instance + + raise("Unknown target type #{type}") unless [:broadcast, :directed, :reply].include?(type) + + if collective.nil? + config.collectives.map do |c| + {:agent => agent, :type => type, :collective => c} + end + else + raise("Unknown collective '#{collective}' known collectives are '#{config.collectives.join ', '}'") unless config.collectives.include?(collective) + + [{:agent => agent, :type => type, :collective => collective}] + end + end + + # Helper to subscribe to a topic on multiple collectives or just one + def self.subscribe(targets) + connection = PluginManager["connector_plugin"] + + targets = [targets].flatten + + targets.each do |target| + connection.subscribe(target[:agent], target[:type], target[:collective]) + end + end + + # Helper to unsubscribe to a topic on multiple collectives or just one + def self.unsubscribe(targets) + connection = PluginManager["connector_plugin"] + + targets = [targets].flatten + + targets.each do |target| + connection.unsubscribe(target[:agent], target[:type], target[:collective]) + end + end + + # Wrapper around PluginManager.loadclass + def self.loadclass(klass) + PluginManager.loadclass(klass) + end + + # Parse a fact filter string like foo=bar into the tuple hash thats needed + def self.parse_fact_string(fact) + if fact =~ /^([^ ]+?)[ ]*=>[ ]*(.+)/ + return {:fact => $1, :value => $2, :operator => '>=' } + elsif fact =~ /^([^ ]+?)[ ]*=<[ ]*(.+)/ + return {:fact => $1, :value => $2, :operator => '<=' } + elsif fact =~ /^([^ ]+?)[ ]*(<=|>=|<|>|!=|==|=~)[ ]*(.+)/ + return {:fact => $1, :value => $3, :operator => $2 } + elsif fact =~ /^(.+?)[ ]*=[ ]*\/(.+)\/$/ + return {:fact => $1, :value => "/#{$2}/", :operator => '=~' } + elsif fact =~ /^([^= ]+?)[ ]*=[ ]*(.+)/ + return {:fact => $1, :value => $2, :operator => '==' } + else + raise "Could not parse fact #{fact} it does not appear to be in a valid format" + end + end + + # Escapes a string so it's safe to use in system() or backticks + # + # Taken from Shellwords#shellescape since it's only in a few ruby versions + def self.shellescape(str) + return "''" if str.empty? + + str = str.dup + + # Process as a single byte sequence because not all shell + # implementations are multibyte aware. + str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1") + + # A LF cannot be escaped with a backslash because a backslash + LF + # combo is regarded as line continuation and simply ignored. + str.gsub!(/\n/, "'\n'") + + return str + end + + def self.windows? + !!(RbConfig::CONFIG['host_os'] =~ /mswin|win32|dos|mingw|cygwin/i) + end + + # Return color codes, if the config color= option is false + # just return a empty string + def self.color(code) + colorize = Config.instance.color + + colors = {:red => "", + :green => "", + :yellow => "", + :cyan => "", + :bold => "", + :reset => ""} + + if colorize + return colors[code] || "" + else + return "" + end + end + + # Helper to return a string in specific color + def self.colorize(code, msg) + "%s%s%s" % [ color(code), msg, color(:reset) ] + end + + # Returns the current ruby version as per RUBY_VERSION, mostly + # doing this here to aid testing + def self.ruby_version + RUBY_VERSION + end + + def self.mcollective_version + MCollective::VERSION + end + + # Returns an aligned_string of text relative to the size of the terminal + # window. If a line in the string exceeds the width of the terminal window + # the line will be chopped off at the whitespace chacter closest to the + # end of the line and prepended to the next line, keeping all indentation. + # + # The terminal size is detected by default, but custom line widths can + # passed. All strings will also be left aligned with 5 whitespace characters + # by default. + def self.align_text(text, console_cols = nil, preamble = 5) + unless console_cols + console_cols = terminal_dimensions[0] + + # if unknown size we default to the typical unix default + console_cols = 80 if console_cols == 0 + end + + console_cols -= preamble + + # Return unaligned text if console window is too small + return text if console_cols <= 0 + + # If console is 0 this implies unknown so we assume the common + # minimal unix configuration of 80 characters + console_cols = 80 if console_cols <= 0 + + text = text.split("\n") + piece = '' + whitespace = 0 + + text.each_with_index do |line, i| + whitespace = 0 + + while whitespace < line.length && line[whitespace].chr == ' ' + whitespace += 1 + end + + # If the current line is empty, indent it so that a snippet + # from the previous line is aligned correctly. + if line == "" + line = (" " * whitespace) + end + + # If text was snipped from the previous line, prepend it to the + # current line after any current indentation. + if piece != '' + # Reset whitespaces to 0 if there are more whitespaces than there are + # console columns + whitespace = 0 if whitespace >= console_cols + + # If the current line is empty and being prepended to, create a new + # empty line in the text so that formatting is preserved. + if text[i + 1] && line == (" " * whitespace) + text.insert(i + 1, "") + end + + # Add the snipped text to the current line + line.insert(whitespace, "#{piece} ") + end + + piece = '' + + # Compare the line length to the allowed line length. + # If it exceeds it, snip the offending text from the line + # and store it so that it can be prepended to the next line. + if line.length > (console_cols + preamble) + reverse = console_cols + + while line[reverse].chr != ' ' + reverse -= 1 + end + + piece = line.slice!(reverse, (line.length - 1)).lstrip + end + + # If a snippet exists when all the columns in the text have been + # updated, create a new line and append the snippet to it, using + # the same left alignment as the last line in the text. + if piece != '' && text[i+1].nil? + text[i+1] = "#{' ' * (whitespace)}#{piece}" + piece = '' + end + + # Add the preamble to the line and add it to the text + line = ((' ' * preamble) + line) + text[i] = line + end + + text.join("\n") + end + + # Figures out the columns and lines of the current tty + # + # Returns [0, 0] if it can't figure it out or if you're + # not running on a tty + def self.terminal_dimensions(stdout = STDOUT, environment = ENV) + return [0, 0] unless stdout.tty? + + return [80, 40] if Util.windows? + + if environment["COLUMNS"] && environment["LINES"] + return [environment["COLUMNS"].to_i, environment["LINES"].to_i] + + elsif environment["TERM"] && command_in_path?("tput") + return [`tput cols`.to_i, `tput lines`.to_i] + + elsif command_in_path?('stty') + return `stty size`.scan(/\d+/).map {|s| s.to_i } + else + return [0, 0] + end + rescue + [0, 0] + end + + # Checks in PATH returns true if the command is found + def self.command_in_path?(command) + found = ENV["PATH"].split(File::PATH_SEPARATOR).map do |p| + File.exist?(File.join(p, command)) + end + + found.include?(true) + end + + # compare two software versions as commonly found in + # package versions. + # + # returns 0 if a == b + # returns -1 if a < b + # returns 1 if a > b + # + # Code originally from Puppet but refactored to a more + # ruby style that fits in better with this code base + def self.versioncmp(version_a, version_b) + vre = /[-.]|\d+|[^-.\d]+/ + ax = version_a.scan(vre) + bx = version_b.scan(vre) + + until ax.empty? || bx.empty? + a = ax.shift + b = bx.shift + + next if a == b + next if a == '-' && b == '-' + return -1 if a == '-' + return 1 if b == '-' + next if a == '.' && b == '.' + return -1 if a == '.' + return 1 if b == '.' + + if a =~ /^[^0]\d+$/ && b =~ /^[^0]\d+$/ + return Integer(a) <=> Integer(b) + else + return a.upcase <=> b.upcase + end + end + + version_a <=> version_b + end + + # we should really use Pathname#absolute? but it's not in all the + # ruby versions we support and it comes down to roughly this + def self.absolute_path?(path, separator=File::SEPARATOR, alt_separator=File::ALT_SEPARATOR) + if alt_separator + path_matcher = /^[#{Regexp.quote alt_separator}#{Regexp.quote separator}]/ + else + path_matcher = /^#{Regexp.quote separator}/ + end + + !!path.match(path_matcher) + end + + # Looks up and interprolate the hash values into a i18n string + def self.t(msgid, args={}) + if msgid.is_a?(Symbol) + I18n.t("%s.pattern" % msgid, args) + else + I18n.t(msgid, args) + end + end + end +end diff --git a/lib/mcollective/validator.rb b/lib/mcollective/validator.rb new file mode 100644 index 0000000..f83c606 --- /dev/null +++ b/lib/mcollective/validator.rb @@ -0,0 +1,80 @@ +module MCollective + module Validator + @last_load = nil + + # Loads the validator plugins. Validators will only be loaded every 5 minutes + def self.load_validators + if load_validators? + @last_load = Time.now.to_i + PluginManager.find_and_load("validator") + end + end + + # Returns and instance of the Plugin class from which objects can be created. + # Valid plugin names are + # :valplugin + # "valplugin" + # "ValpluginValidator" + def self.[](klass) + if klass.is_a?(Symbol) + klass = validator_class(klass) + elsif !(klass.match(/.*Validator$/)) + klass = validator_class(klass) + end + + const_get(klass) + end + + # Allows validation plugins to be called like module methods : Validator.validate() + def self.method_missing(method, *args, &block) + if has_validator?(method) + validator = Validator[method].validate(*args) + else + raise ValidatorError, "Unknown validator: '#{method}'." + end + end + + def self.has_validator?(validator) + const_defined?(validator_class(validator)) + end + + def self.validator_class(validator) + "#{validator.to_s.capitalize}Validator" + end + + def self.load_validators? + return true if @last_load.nil? + + (@last_load - Time.now.to_i) > 300 + end + + # Generic validate method that will call the correct validator + # plugin based on the type of the validation parameter + def self.validate(validator, validation) + Validator.load_validators + + begin + if [:integer, :boolean, :float, :number, :string].include?(validation) + Validator.typecheck(validator, validation) + + else + case validation + when Regexp,String + Validator.regex(validator, validation) + + when Symbol + Validator.send(validation, validator) + + when Array + Validator.array(validator, validation) + + when Class + Validator.typecheck(validator, validation) + end + end + rescue => e + raise ValidatorError, e.to_s + end + end + end +end diff --git a/lib/mcollective/vendor.rb b/lib/mcollective/vendor.rb new file mode 100644 index 0000000..0451467 --- /dev/null +++ b/lib/mcollective/vendor.rb @@ -0,0 +1,41 @@ +module MCollective + # Simple module to manage vendored code. + # + # To vendor a library simply download its whole git repo or untar + # into vendor/libraryname and create a load_libraryname.rb file + # to add its libdir into the $:. + # + # Once you have that file, add a require line in vendor/require_vendored.rb + # which will run after all the load_* files. + # + # The intention is to not change vendored libraries and to eventually + # make adding them in optional so that distros can simply adjust their + # packaging to exclude this directory and the various load_xxx.rb scripts + # if they wish to install these gems as native packages. + class Vendor + class << self + def vendor_dir + File.join([File.dirname(File.expand_path(__FILE__)), "vendor"]) + end + + def load_entry(entry) + Log.debug("Loading vendored #{$1}") + load "#{vendor_dir}/#{entry}" + end + + def require_libs + require 'mcollective/vendor/require_vendored' + end + + def load_vendored + Dir.entries(vendor_dir).each do |entry| + if entry.match(/load_(\w+?)\.rb$/) + load_entry entry + end + end + + require_libs + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/.gitignore b/lib/mcollective/vendor/i18n/.gitignore new file mode 100644 index 0000000..b066d1e --- /dev/null +++ b/lib/mcollective/vendor/i18n/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +test/rails/fixtures +nbproject/ +vendor/**/* +*.swp +pkg +.bundle +.rvmrc diff --git a/lib/mcollective/vendor/i18n/.travis.yml b/lib/mcollective/vendor/i18n/.travis.yml new file mode 100644 index 0000000..792ec6c --- /dev/null +++ b/lib/mcollective/vendor/i18n/.travis.yml @@ -0,0 +1,7 @@ +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 + - ree + - rbx + - jruby diff --git a/lib/mcollective/vendor/i18n/CHANGELOG.textile b/lib/mcollective/vendor/i18n/CHANGELOG.textile new file mode 100644 index 0000000..e1eeda8 --- /dev/null +++ b/lib/mcollective/vendor/i18n/CHANGELOG.textile @@ -0,0 +1,152 @@ +h1. Changelog + +h2. 0.5.0 + +* "Extract Backend::ActiveRecord to a separate gem":https://github.com/svenfuchs/i18n/commit/197dacebad356b910d69fa69a719c2ad10cf49e6 (see "i18n-active_record":https://github.com/svenfuchs/i18n-active_record) +* "Improve exception handling":https://github.com/svenfuchs/i18n/commit/2913ff9a7544f223f60e7d7b32c2a0e1af89812b (deprectates I18n.default_exception_handler) +* "Change MissingTranslationData message to 'translation missing: foo.bar'":https://github.com/svenfuchs/i18n/commit/68fdfe47952325411afe5942e971ce10b2bdf900 +* "Expose MissingTranslationsData#keys method":https://github.com/svenfuchs/i18n/commit/3a37a389ecaac9670355b334e23e775549ee9822 +* "Improve Cascade#lookup (add default options)":https://github.com/svenfuchs/i18n/commit/0b9a1f2058a2be9543106cc19d08071c359511e1 +* "Finally remove deprecated interpolation syntax":https://github.com/svenfuchs/i18n/commit/2d43846d2b2a2e596f30fa58ea1c9ddb2243bb64 + +h2. 0.4.2 (2010-10-26) + +* "Improve UTF8 handling":http://github.com/svenfuchs/i18n/commit/e8d5820a3b08eeca28de1a2b9c8a6ad2b9e6476c +* "Expose I18n::VERSION":http://github.com/svenfuchs/i18n/commit/b832037bac94c7144f45f3ff5e3b4e4089781726 +* "Better deprecation output":http://github.com/svenfuchs/i18n/commit/2bee924464b8a9c33d3d7852eb1c8423aa38cc25 + +h2. 0.4.1 (2010-06-05) + +* "Fix interpolation failure on Ruby 1.9":http://github.com/svenfuchs/i18n/commit/8d45bedb11c4136c00e853d104b00a8e67ec4894 + +h2. 0.4.0 (2010-05-27) + +* "The localization proc also receives the object as option":http://github.com/svenfuchs/i18n/commit/4a8cd9fa660daaa3078e24c5851353ca377d9213 + +h2. 0.4.0.beta1 (2010-05-03) + +* "Renamed Fast backend to Memoize backend":http://github.com/svenfuchs/i18n/commit/f7f7dc12c00a19d3876223771e14f8671ff313cd + +* "Deprecate {{}} as interpolation syntax":http://github.com/svenfuchs/i18n/commit/8894ee521ef5788c415b625a6daf522af4c416e0 + +* "Allow nil translation to be stored again":http://github.com/svenfuchs/i18n/commit/f2074f1e82d10c2e9a801c8cc2f2a0c7c30703ba + +h2. 0.4.0.beta (2010-04-30) + +* "Added a KeyValue backend":http://github.com/svenfuchs/i18n/commit/28ca5f53ade7f545f8c0804e93564d4686b416a4 + +* "Added transliteration support":http://github.com/svenfuchs/i18n/commit/928fdb4794959e779e90f360eb01ba043672d8d5 + +* "Create Flatten backend module to aid handling flatten translations":http://github.com/svenfuchs/i18n/commit/2ec9d6998aa8facd7b15a3ef47a96cf2471cd8a1 + +* "Decouple the external separator (used when storing translations) from the internal separator in Fast and ActiveRecord backends":http://github.com/svenfuchs/i18n/commit/274cb4daa0ca5e3b2bd23b45eb7f9fc58f75a79d + +h2. 0.3.7 (2010-04-17) + +* "Speed up I18n.normalize_keys by caching reused normalizations and producing less garbage":http://github.com/svenfuchs/i18n/commit/819dac0fea9c29e6545801aa107e63e355728cd4 + +h2. 0.3.6 (2010-03-23) + +* "Move gettext po parser to lib":http://github.com/svenfuchs/i18n/commit/b2f038663b55727ac2327e6f07a46ba5d69d600c + +* "Move I18n configuration to I18n.config":http://github.com/svenfuchs/i18n/commit/4a7baea86663ead8c681008c3e80a622f0546b07 + +h2. 0.3.5 (2010-02-26) + +* "Delegate I18n.normalize_translation_keys to I18n.normalize_keys and deprecate +the former":http://github.com/svenfuchs/i18n/commit/7284b04d5f5dd9679cb68875515cdd0cdfc96fef + +h2. 0.3.4 (2010-02-25) + +* "Rename I18n.normalize_translation_keys to I18n.normalize_keys and finally make it public":http://github.com/svenfuchs/i18n/commit/20b05fe5802df6c90fb70a4e3760b2b851b791b3 + +* "Added CLDR supoprt":http://github.com/svenfuchs/i18n/commit/860eadf671a231e7f5dffb1bb27fa318ff7a8786 + +h2. 0.3.3 (2009-12-29) + +* "Use lib/i18n/version":http://github.com/svenfuchs/i18n/commit/ff426c8e7a2438b814cb303adadec292dacb752e + +* "Added a benchmark suite":http://github.com/svenfuchs/i18n/commit/f9b5b9b113097724638bdab96862ffa404e67e70 + +* "Ensure links can be handled recursively":http://github.com/svenfuchs/i18n/commit/2c50bd209f3fc24fe9dfa694c81be64340f09b7d + +* "Make sure we can lookup false values as translation data":http://github.com/svenfuchs/i18n/commit/561c82ba4b8921d03bfdf56cb2d0c2f287629001 + +* "Added Fast backend module":http://github.com/svenfuchs/i18n/commit/bd2f09f0a251ca793b0e8ecc7e32177a2f091c23 + +* "Added InterpolationCompiler backend module":http://github.com/svenfuchs/i18n/commit/91810887d1abfb28996a9183bc9004678290d28b + +h2. 0.3.2 (2009-12-12) + +* "Added Cascade backend":http://github.com/svenfuchs/i18n/commit/8009aef293e9ef8564c9005090d8380feabcaf6f + +h2. 0.3.1 (2009-12-11) + +* "Add PoParser to gemspec":http://github.com/svenfuchs/i18n/commit/d6b2763f39c932f66adb039b96882a472f883c51 +* "Enable custom separators for ActiveRecord backend":http://github.com/svenfuchs/i18n/commit/9341d3fcfc951cc31807ba672d2b5d90909ef3e5 +* "Pass interpolation values to interpolation procs":http://github.com/svenfuchs/i18n/commit/39c2ed8fbad645671cd5520ce7ad0aeefe2b0cca +* "Fix that ngettext supports keys with dots":http://github.com/svenfuchs/i18n/commit/7362a43c34364d500de8899cfcca6bf1a5e6d1c8 + +h2. 0.3.0 (2009-11-30) + +* "Gettext backend and helpers":http://github.com/svenfuchs/i18n/commit/35a1740d2f10b808548af352006950da4017e374 +* "Metadata module":http://github.com/svenfuchs/i18n/commit/2677208555179b36fcbe958c0e8bc642cf5bc020 +* "Basic ActiveRecord backend":http://github.com/svenfuchs/i18n/commit/786632d0b42de423ecf0969622efc87f1691e2a2 +* "Set encoding to UTF8 for all files":http://github.com/svenfuchs/i18n/commit/9be3d4a311b5bf583eec5d39986176cc40c112f2 +* "Chain backend":http://github.com/svenfuchs/i18n/commit/08259ffb88b3005403648d77bc1cbca0b92f3cf5 +* "Backend/cache implementation":http://github.com/svenfuchs/i18n/commit/e7bf15351cd2e27f5972eb40e65a5dd6f4a0feed +* "Pluralization module":http://github.com/svenfuchs/i18n/commit/9ca4c9ed52d4706566a6abeb2d78722dcc5d4764 +* "add and adapt Globalize2 fallback implementation":http://github.com/svenfuchs/i18n/commit/1b37a303b27d6222b17162804b06323e5628768f +* "move Simple backend implementation to a Base backend class and extend Simple from Base.":http://github.com/svenfuchs/i18n/commit/32ddc80a04e6aa247f6d6613bde7f78c73396cb4 + +h2. 0.2.0 (2009-07-12) + +* "Allow using Ruby 1.9 syntax for string interpolation (API addition)":http://github.com/svenfuchs/i18n/commit/c6e0b06d512f2af57199a843a1d8a40241b32861 +* "Allow configuring the default scope separator, allow to pass a custom scope separator(API addition)":http://github.com/svenfuchs/i18n/commit/5b75bfbc348061adc11e3790187a187275bfd471 (e.g. I18n.t(:'foo|bar', :separator => '|') +* "Pass :format option to #translate for #localize more useful lambda support":http://github.com/svenfuchs/i18n/commit/e277711b3c844fe7589b8d3f9af0f7d1b969a273 +* "Refactor Simple backend #resolve to #default and #resolve for more consistency. Now allows to pass lambdas as defaults and re-resolve Symbols":http://github.com/svenfuchs/i18n/commit/8c4ce3d923ce5fa73e973fe28217e18165549aba +* "Add lambda support to #translate (API addition)":http://github.com/svenfuchs/i18n/commit/c90e62d8f7d3d5b78f34cfe328d871b58884f115 +* "Add lambda support to #localize (API addition)":http://github.com/svenfuchs/i18n/commit/9d390afcf33f3f469bb95e6888147152f6cc7442 + +h2. 0.1.3 (2009-02-27) + +* "Remove unnecessary string encoding handling in the i18n simple backend which made the backend break on Ruby 1.9":http://github.com/svenfuchs/i18n/commit/4c3a970783861a94f2e89f46714fb3434e4f4f8d + +h2. 0.1.2 (2009-01-09) + +* "added #available_locales (returns an array of locales for which translations are available)":http://github.com/svenfuchs/i18n/commit/411f8fe7c8f3f89e9b6b921fa62ed66cb92f3af4 +* "flatten load_path before using it so that a nested array of paths won't throw up":http://github.com/svenfuchs/i18n/commit/d473a068a2b90aba98135deb225d6eb6d8104d70 + +h2. 0.1.1 (2008-11-20) + +* "Use :'en' as a default locale (in favor of :'en-US')":http://github.com/svenfuchs/i18n/commit/c4b10b246aecf7da78cb2568dd0d2ab7e6b8a230 +* "Add #reload! to Simple backend":http://github.com/svenfuchs/i18n/commit/36dd2bd9973b9e1559728749a9daafa44693e964 + +h2. 0.1.0 (2008-10-25) + +* "Fix Simple backend to distinguish false from nil values":http://github.com/svenfuchs/i18n/commit/39d9a47da14b5f3ba126af48923af8c30e135166 +* "Add #load_path to public api, add initialize to simple backend and remove #load_translations from public api":http://github.com/svenfuchs/i18n/commit/c4c5649e6bc8f020f1aaf5a5470bde048e22c82d +* "Speed up Backend::Simple#interpolate":http://github.com/svenfuchs/i18n/commit/9e1ac6bf8833304e036323ec9932b9f33c468a35 +* "Remove #populate and #store_translations from public API":http://github.com/svenfuchs/i18n/commit/f4e514a80be7feb509f66824ee311905e2940900 +* "Use :other instead of :many as a plural key":http://github.com/svenfuchs/i18n/commit/0f8f20a2552bf6a2aa758d8fdd62a7154e4a1bf6 +* "Use a class instead of a module for Simple backend":http://github.com/svenfuchs/i18n/commit/08f051aa61320c17debde24a83268bc74e33b995 +* "Make Simple backend #interpolate deal with non-ASCII string encodings":http://github.com/svenfuchs/i18n/commit/d84a3f3f55543c084d5dc5d1fed613b8df148789 +* "Fix default arrays of non-existant keys returning the default array":http://github.com/svenfuchs/i18n/commit/6c04ca86c87f97dc78f07c2a4023644e5ba8b839 + +h2. Initial implementation (June/July 2008) + +Initial implementation by "Sven Fuchs":http://www.workingwithrails.com/person/9963-sven-fuchs based on previous discussion/consensus of the rails-i18n team (alphabetical order) and many others: + +* "Matt Aimonetti":http://railsontherun.com +* "Sven Fuchs":http://www.workingwithrails.com/person/9963-sven-fuchs +* "Joshua Harvey":http://www.workingwithrails.com/person/759-joshua-harvey +* "Saimon Moore":http://saimonmoore.net +* "Stephan Soller":http://www.arkanis-development.de + +h2. More information + +* "Homepage":http://rails-i18n.org +* "Wiki":http://rails-i18n.org/wiki +* "Mailinglist":http://groups.google.com/group/rails-i18n +* "About the project/history":http://www.artweb-design.de/2008/7/18/finally-ruby-on-rails-gets-internationalized +* "Initial API Intro":http://www.artweb-design.de/2008/7/18/the-ruby-on-rails-i18n-core-api diff --git a/lib/mcollective/vendor/i18n/MIT-LICENSE b/lib/mcollective/vendor/i18n/MIT-LICENSE new file mode 100755 index 0000000..ed8e9ee --- /dev/null +++ b/lib/mcollective/vendor/i18n/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2008 The Ruby I18n team + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/lib/mcollective/vendor/i18n/README.textile b/lib/mcollective/vendor/i18n/README.textile new file mode 100644 index 0000000..57b79c1 --- /dev/null +++ b/lib/mcollective/vendor/i18n/README.textile @@ -0,0 +1,105 @@ +h1. Ruby I18n + +!https://secure.travis-ci.org/svenfuchs/i18n.png?branch=master(Build Status)!:http://travis-ci.org/svenfuchs/i18n + +Ruby Internationalization and localization solution. + +Features: + +* translation and localization +* interpolation of values to translations (Ruby 1.9 compatible syntax) +* pluralization (CLDR compatible) +* customizable transliteration to ASCII +* flexible defaults +* bulk lookup +* lambdas as translation data +* custom key/scope separator +* custom exception handlers +* extensible architecture with a swappable backend + +Pluggable features: + +* Cache +* Pluralization: lambda pluralizers stored as translation data +* Locale fallbacks, RFC4647 compliant (optionally: RFC4646 locale validation) +* Gettext support +* Translation metadata + +Alternative backends: + +* Chain +* ActiveRecord (optionally: ActiveRecord::Missing and ActiveRecord::StoreProcs) +* KeyValue (uses active_support/json and cannot store procs) + +For more information and lots of resources see: "http://ruby-i18n.org/wiki":http://ruby-i18n.org/wiki + +h2. Installation + +gem install i18n + +h4. Rails version warning + +On Rails < 2.3.6 the method I18n.localize will fail with MissingInterpolationArgument (issue "20":http://github.com/svenfuchs/i18n/issues/issue/20). Upgrade to Rails 2.3.6 or higher (2.3.8 preferably) is recommended. + +h3. Installation on Rails < 2.3.5 (deprecated) + +Up to version 2.3.4 Rails will not accept i18n gems > 0.1.3. There is an unpacked +gem inside of active_support/lib/vendor which gets loaded unless gem 'i18n', '~> 0.1.3'. +This requirement is relaxed in "6da03653":http://github.com/rails/rails/commit/6da03653 + +The new i18n gem can be loaded from vendor/plugins like this: + +
+  def reload_i18n!
+    raise "Move to i18n version 0.2.0 or greater" if Rails.version > "2.3.4"
+
+    $:.grep(/i18n/).each { |path| $:.delete(path) }
+    I18n::Backend.send :remove_const, "Simple"
+    $: << Rails.root.join('vendor', 'plugins', 'i18n', 'lib').to_s
+  end
+
+ +Then you can `reload_i18n!` inside an i18n initializer. + +h2. Tests + +You can run tests both with + +* `rake test` or just `rake` +* run any test file directly, e.g. `ruby -Ilib -Itest test/api/simple_test.rb` +* run all tests with `ruby -Ilib -Itest test/all.rb` + +You can run all tests against all Gemfiles with + +* `ruby test/run_all.rb` + +The structure of the test suite is a bit unusual as it uses modules to reuse +particular tests in different test cases. + +The reason for this is that we need to enforce the I18n API across various +combinations of extensions. E.g. the Simple backend alone needs to support +the same API as any combination of feature and/or optimization modules included +to the Simple backend. We test this by reusing the same API defition (implemented +as test methods) in test cases with different setups. + +You can find the test cases that enforce the API in test/api. And you can find +the API definition test methods in test/api/tests. + +All other test cases (e.g. as defined in test/backend, test/core\_ext) etc. +follow the usual test setup and should be easy to grok. + +h2. Authors + +* "Sven Fuchs":http://www.artweb-design.de +* "Joshua Harvey":http://www.workingwithrails.com/person/759-joshua-harvey +* "Stephan Soller":http://www.arkanis-development.de +* "Saimon Moore":http://saimonmoore.net +* "Matt Aimonetti":http://railsontherun.com + +h2. Contributors + +http://github.com/svenfuchs/i18n/contributors + +h2. License + +MIT License. See the included MIT-LICENSE file. diff --git a/lib/mcollective/vendor/i18n/Rakefile b/lib/mcollective/vendor/i18n/Rakefile new file mode 100644 index 0000000..9403607 --- /dev/null +++ b/lib/mcollective/vendor/i18n/Rakefile @@ -0,0 +1,27 @@ +require 'rake/testtask' + +task :default => [:test] + +Rake::TestTask.new(:test) do |t| + t.libs << 'lib' + t.libs << 'test' + t.pattern = "#{File.dirname(__FILE__)}/test/all.rb" + t.verbose = true + t.warning = true +end +Rake::Task['test'].comment = "Run all i18n tests" + +# require "rake/gempackagetask" +# require "rake/clean" +# CLEAN << "pkg" << "doc" << "coverage" << ".yardoc" +# +# Rake::GemPackageTask.new(eval(File.read("i18n.gemspec"))) { |pkg| } +# +# begin +# require "yard" +# YARD::Rake::YardocTask.new do |t| +# t.options = ["--output-dir=doc"] +# t.options << "--files" << ["CHANGELOG.textile", "contributors.txt", "MIT-LICENSE"].join(",") +# end +# rescue LoadError +# end diff --git a/lib/mcollective/vendor/i18n/lib/i18n.rb b/lib/mcollective/vendor/i18n/lib/i18n.rb new file mode 100755 index 0000000..a351b4a --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n.rb @@ -0,0 +1,332 @@ +require 'i18n/version' +require 'i18n/exceptions' +require 'i18n/interpolate/ruby' + +module I18n + autoload :Backend, 'i18n/backend' + autoload :Config, 'i18n/config' + autoload :Gettext, 'i18n/gettext' + autoload :Locale, 'i18n/locale' + autoload :Tests, 'i18n/tests' + + RESERVED_KEYS = [:scope, :default, :separator, :resolve, :object, :fallback, :format, :cascade, :throw, :raise, :rescue_format] + RESERVED_KEYS_PATTERN = /%\{(#{RESERVED_KEYS.join("|")})\}/ + + extend Module.new { + # Gets I18n configuration object. + def config + Thread.current[:i18n_config] ||= I18n::Config.new + end + + # Sets I18n configuration object. + def config=(value) + Thread.current[:i18n_config] = value + end + + # Write methods which delegates to the configuration object + %w(locale backend default_locale available_locales default_separator + exception_handler load_path).each do |method| + module_eval <<-DELEGATORS, __FILE__, __LINE__ + 1 + def #{method} + config.#{method} + end + + def #{method}=(value) + config.#{method} = (value) + end + DELEGATORS + end + + # Tells the backend to reload translations. Used in situations like the + # Rails development environment. Backends can implement whatever strategy + # is useful. + def reload! + config.backend.reload! + end + + # Translates, pluralizes and interpolates a given key using a given locale, + # scope, and default, as well as interpolation values. + # + # *LOOKUP* + # + # Translation data is organized as a nested hash using the upper-level keys + # as namespaces. E.g., ActionView ships with the translation: + # :date => {:formats => {:short => "%b %d"}}. + # + # Translations can be looked up at any level of this hash using the key argument + # and the scope option. E.g., in this example I18n.t :date + # returns the whole translations hash {:formats => {:short => "%b %d"}}. + # + # Key can be either a single key or a dot-separated key (both Strings and Symbols + # work). E.g., the short format can be looked up using both: + # I18n.t 'date.formats.short' + # I18n.t :'date.formats.short' + # + # Scope can be either a single key, a dot-separated key or an array of keys + # or dot-separated keys. Keys and scopes can be combined freely. So these + # examples will all look up the same short date format: + # I18n.t 'date.formats.short' + # I18n.t 'formats.short', :scope => 'date' + # I18n.t 'short', :scope => 'date.formats' + # I18n.t 'short', :scope => %w(date formats) + # + # *INTERPOLATION* + # + # Translations can contain interpolation variables which will be replaced by + # values passed to #translate as part of the options hash, with the keys matching + # the interpolation variable names. + # + # E.g., with a translation :foo => "foo %{bar}" the option + # value for the key +bar+ will be interpolated into the translation: + # I18n.t :foo, :bar => 'baz' # => 'foo baz' + # + # *PLURALIZATION* + # + # Translation data can contain pluralized translations. Pluralized translations + # are arrays of singluar/plural versions of translations like ['Foo', 'Foos']. + # + # Note that I18n::Backend::Simple only supports an algorithm for English + # pluralization rules. Other algorithms can be supported by custom backends. + # + # This returns the singular version of a pluralized translation: + # I18n.t :foo, :count => 1 # => 'Foo' + # + # These both return the plural version of a pluralized translation: + # I18n.t :foo, :count => 0 # => 'Foos' + # I18n.t :foo, :count => 2 # => 'Foos' + # + # The :count option can be used both for pluralization and interpolation. + # E.g., with the translation + # :foo => ['%{count} foo', '%{count} foos'], count will + # be interpolated to the pluralized translation: + # I18n.t :foo, :count => 1 # => '1 foo' + # + # *DEFAULTS* + # + # This returns the translation for :foo or default if no translation was found: + # I18n.t :foo, :default => 'default' + # + # This returns the translation for :foo or the translation for :bar if no + # translation for :foo was found: + # I18n.t :foo, :default => :bar + # + # Returns the translation for :foo or the translation for :bar + # or default if no translations for :foo and :bar were found. + # I18n.t :foo, :default => [:bar, 'default'] + # + # *BULK LOOKUP* + # + # This returns an array with the translations for :foo and :bar. + # I18n.t [:foo, :bar] + # + # Can be used with dot-separated nested keys: + # I18n.t [:'baz.foo', :'baz.bar'] + # + # Which is the same as using a scope option: + # I18n.t [:foo, :bar], :scope => :baz + # + # *LAMBDAS* + # + # Both translations and defaults can be given as Ruby lambdas. Lambdas will be + # called and passed the key and options. + # + # E.g. assuming the key :salutation resolves to: + # lambda { |key, options| options[:gender] == 'm' ? "Mr. %{options[:name]}" : "Mrs. %{options[:name]}" } + # + # Then I18n.t(:salutation, :gender => 'w', :name => 'Smith') will result in "Mrs. Smith". + # + # It is recommended to use/implement lambdas in an "idempotent" way. E.g. when + # a cache layer is put in front of I18n.translate it will generate a cache key + # from the argument values passed to #translate. Therefor your lambdas should + # always return the same translations/values per unique combination of argument + # values. + def translate(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + key = args.shift + backend = config.backend + locale = options.delete(:locale) || config.locale + handling = options.delete(:throw) && :throw || options.delete(:raise) && :raise # TODO deprecate :raise + + raise I18n::ArgumentError if key.is_a?(String) && key.empty? + + result = catch(:exception) do + if key.is_a?(Array) + key.map { |k| backend.translate(locale, k, options) } + else + backend.translate(locale, key, options) + end + end + result.is_a?(MissingTranslation) ? handle_exception(handling, result, locale, key, options) : result + end + alias :t :translate + + # Wrapper for translate that adds :raise => true. With + # this option, if no translation is found, it will raise I18n::MissingTranslationData + def translate!(key, options={}) + translate(key, options.merge(:raise => true)) + end + alias :t! :translate! + + # Transliterates UTF-8 characters to ASCII. By default this method will + # transliterate only Latin strings to an ASCII approximation: + # + # I18n.transliterate("Ærøskøbing") + # # => "AEroskobing" + # + # I18n.transliterate("日本語") + # # => "???" + # + # It's also possible to add support for per-locale transliterations. I18n + # expects transliteration rules to be stored at + # i18n.transliterate.rule. + # + # Transliteration rules can either be a Hash or a Proc. Procs must accept a + # single string argument. Hash rules inherit the default transliteration + # rules, while Procs do not. + # + # *Examples* + # + # Setting a Hash in .yml: + # + # i18n: + # transliterate: + # rule: + # ü: "ue" + # ö: "oe" + # + # Setting a Hash using Ruby: + # + # store_translations(:de, :i18n => { + # :transliterate => { + # :rule => { + # "ü" => "ue", + # "ö" => "oe" + # } + # } + # ) + # + # Setting a Proc: + # + # translit = lambda {|string| MyTransliterator.transliterate(string) } + # store_translations(:xx, :i18n => {:transliterate => {:rule => translit}) + # + # Transliterating strings: + # + # I18n.locale = :en + # I18n.transliterate("Jürgen") # => "Jurgen" + # I18n.locale = :de + # I18n.transliterate("Jürgen") # => "Juergen" + # I18n.transliterate("Jürgen", :locale => :en) # => "Jurgen" + # I18n.transliterate("Jürgen", :locale => :de) # => "Juergen" + def transliterate(*args) + options = args.pop if args.last.is_a?(Hash) + key = args.shift + locale = options && options.delete(:locale) || config.locale + handling = options && (options.delete(:throw) && :throw || options.delete(:raise) && :raise) + replacement = options && options.delete(:replacement) + config.backend.transliterate(locale, key, replacement) + rescue I18n::ArgumentError => exception + handle_exception(handling, exception, locale, key, options || {}) + end + + # Localizes certain objects, such as dates and numbers to local formatting. + def localize(object, options = {}) + locale = options.delete(:locale) || config.locale + format = options.delete(:format) || :default + config.backend.localize(locale, object, format, options) + end + alias :l :localize + + # Executes block with given I18n.locale set. + def with_locale(tmp_locale = nil) + if tmp_locale + current_locale = self.locale + self.locale = tmp_locale + end + yield + ensure + self.locale = current_locale if tmp_locale + end + + # Merges the given locale, key and scope into a single array of keys. + # Splits keys that contain dots into multiple keys. Makes sure all + # keys are Symbols. + def normalize_keys(locale, key, scope, separator = nil) + separator ||= I18n.default_separator + + keys = [] + keys.concat normalize_key(locale, separator) + keys.concat normalize_key(scope, separator) + keys.concat normalize_key(key, separator) + keys + end + + # making these private until Ruby 1.9.2 can send to protected methods again + # see http://redmine.ruby-lang.org/repositories/revision/ruby-19?rev=24280 + private + + # Any exceptions thrown in translate will be sent to the @@exception_handler + # which can be a Symbol, a Proc or any other Object unless they're forced to + # be raised or thrown (MissingTranslation). + # + # If exception_handler is a Symbol then it will simply be sent to I18n as + # a method call. A Proc will simply be called. In any other case the + # method #call will be called on the exception_handler object. + # + # Examples: + # + # I18n.exception_handler = :default_exception_handler # this is the default + # I18n.default_exception_handler(exception, locale, key, options) # will be called like this + # + # I18n.exception_handler = lambda { |*args| ... } # a lambda + # I18n.exception_handler.call(exception, locale, key, options) # will be called like this + # + # I18n.exception_handler = I18nExceptionHandler.new # an object + # I18n.exception_handler.call(exception, locale, key, options) # will be called like this + def handle_exception(handling, exception, locale, key, options) + case handling + when :raise + raise(exception.respond_to?(:to_exception) ? exception.to_exception : exception) + when :throw + throw :exception, exception + else + case handler = options[:exception_handler] || config.exception_handler + when Symbol + send(handler, exception, locale, key, options) + else + handler.call(exception, locale, key, options) + end + end + end + + def normalize_key(key, separator) + normalized_key_cache[separator][key] ||= + case key + when Array + key.map { |k| normalize_key(k, separator) }.flatten + else + keys = key.to_s.split(separator) + keys.delete('') + keys.map! { |k| k.to_sym } + keys + end + end + + def normalized_key_cache + @normalized_key_cache ||= Hash.new { |h,k| h[k] = {} } + end + + # DEPRECATED. Use I18n.normalize_keys instead. + def normalize_translation_keys(locale, key, scope, separator = nil) + puts "I18n.normalize_translation_keys is deprecated. Please use the class I18n.normalize_keys instead." + normalize_keys(locale, key, scope, separator) + end + + # DEPRECATED. Please use the I18n::ExceptionHandler class instead. + def default_exception_handler(exception, locale, key, options) + puts "I18n.default_exception_handler is deprecated. Please use the class I18n::ExceptionHandler instead " + + "(an instance of which is set to I18n.exception_handler by default)." + exception.is_a?(MissingTranslation) ? exception.message : raise(exception) + end + } +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend.rb new file mode 100644 index 0000000..46ef054 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend.rb @@ -0,0 +1,18 @@ +module I18n + module Backend + autoload :Base, 'i18n/backend/base' + autoload :InterpolationCompiler, 'i18n/backend/interpolation_compiler' + autoload :Cache, 'i18n/backend/cache' + autoload :Cascade, 'i18n/backend/cascade' + autoload :Chain, 'i18n/backend/chain' + autoload :Fallbacks, 'i18n/backend/fallbacks' + autoload :Flatten, 'i18n/backend/flatten' + autoload :Gettext, 'i18n/backend/gettext' + autoload :KeyValue, 'i18n/backend/key_value' + autoload :Memoize, 'i18n/backend/memoize' + autoload :Metadata, 'i18n/backend/metadata' + autoload :Pluralization, 'i18n/backend/pluralization' + autoload :Simple, 'i18n/backend/simple' + autoload :Transliterator, 'i18n/backend/transliterator' + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/base.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/base.rb new file mode 100644 index 0000000..0b6217c --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/base.rb @@ -0,0 +1,181 @@ +require 'yaml' +require 'i18n/core_ext/hash' +require 'i18n/core_ext/kernel/surpress_warnings' + +module I18n + module Backend + module Base + include I18n::Backend::Transliterator + + # Accepts a list of paths to translation files. Loads translations from + # plain Ruby (*.rb) or YAML files (*.yml). See #load_rb and #load_yml + # for details. + def load_translations(*filenames) + filenames = I18n.load_path if filenames.empty? + filenames.flatten.each { |filename| load_file(filename) } + end + + # This method receives a locale, a data hash and options for storing translations. + # Should be implemented + def store_translations(locale, data, options = {}) + raise NotImplementedError + end + + def translate(locale, key, options = {}) + raise InvalidLocale.new(locale) unless locale + entry = key && lookup(locale, key, options[:scope], options) + + if options.empty? + entry = resolve(locale, key, entry, options) + else + count, default = options.values_at(:count, :default) + values = options.except(*RESERVED_KEYS) + entry = entry.nil? && default ? + default(locale, key, default, options) : resolve(locale, key, entry, options) + end + + throw(:exception, I18n::MissingTranslation.new(locale, key, options)) if entry.nil? + entry = entry.dup if entry.is_a?(String) + + entry = pluralize(locale, entry, count) if count + entry = interpolate(locale, entry, values) if values + entry + end + + # Acts the same as +strftime+, but uses a localized version of the + # format string. Takes a key from the date/time formats translations as + # a format argument (e.g., :short in :'date.formats'). + def localize(locale, object, format = :default, options = {}) + raise ArgumentError, "Object must be a Date, DateTime or Time object. #{object.inspect} given." unless object.respond_to?(:strftime) + + if Symbol === format + key = format + type = object.respond_to?(:sec) ? 'time' : 'date' + options = options.merge(:raise => true, :object => object, :locale => locale) + format = I18n.t(:"#{type}.formats.#{key}", options) + end + + # format = resolve(locale, object, format, options) + format = format.to_s.gsub(/%[aAbBp]/) do |match| + case match + when '%a' then I18n.t(:"date.abbr_day_names", :locale => locale, :format => format)[object.wday] + when '%A' then I18n.t(:"date.day_names", :locale => locale, :format => format)[object.wday] + when '%b' then I18n.t(:"date.abbr_month_names", :locale => locale, :format => format)[object.mon] + when '%B' then I18n.t(:"date.month_names", :locale => locale, :format => format)[object.mon] + when '%p' then I18n.t(:"time.#{object.hour < 12 ? :am : :pm}", :locale => locale, :format => format) if object.respond_to? :hour + end + end + + object.strftime(format) + end + + # Returns an array of locales for which translations are available + # ignoring the reserved translation meta data key :i18n. + def available_locales + raise NotImplementedError + end + + def reload! + @skip_syntax_deprecation = false + end + + protected + + # The method which actually looks up for the translation in the store. + def lookup(locale, key, scope = [], options = {}) + raise NotImplementedError + end + + # Evaluates defaults. + # If given subject is an Array, it walks the array and returns the + # first translation that can be resolved. Otherwise it tries to resolve + # the translation directly. + def default(locale, object, subject, options = {}) + options = options.dup.reject { |key, value| key == :default } + case subject + when Array + subject.each do |item| + result = resolve(locale, object, item, options) and return result + end and nil + else + resolve(locale, object, subject, options) + end + end + + # Resolves a translation. + # If the given subject is a Symbol, it will be translated with the + # given options. If it is a Proc then it will be evaluated. All other + # subjects will be returned directly. + def resolve(locale, object, subject, options = {}) + return subject if options[:resolve] == false + result = catch(:exception) do + case subject + when Symbol + I18n.translate(subject, options.merge(:locale => locale, :throw => true)) + when Proc + date_or_time = options.delete(:object) || object + resolve(locale, object, subject.call(date_or_time, options)) + else + subject + end + end + result unless result.is_a?(MissingTranslation) + end + + # Picks a translation from an array according to English pluralization + # rules. It will pick the first translation if count is not equal to 1 + # and the second translation if it is equal to 1. Other backends can + # implement more flexible or complex pluralization rules. + def pluralize(locale, entry, count) + return entry unless entry.is_a?(Hash) && count + + key = :zero if count == 0 && entry.has_key?(:zero) + key ||= count == 1 ? :one : :other + raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key) + entry[key] + end + + # Interpolates values into a given string. + # + # interpolate "file %{file} opened by %%{user}", :file => 'test.txt', :user => 'Mr. X' + # # => "file test.txt opened by %{user}" + def interpolate(locale, string, values = {}) + if string.is_a?(::String) && !values.empty? + I18n.interpolate(string, values) + else + string + end + end + + # Loads a single translations file by delegating to #load_rb or + # #load_yml depending on the file extension and directly merges the + # data to the existing translations. Raises I18n::UnknownFileType + # for all other file extensions. + def load_file(filename) + type = File.extname(filename).tr('.', '').downcase + raise UnknownFileType.new(type, filename) unless respond_to?(:"load_#{type}", true) + data = send(:"load_#{type}", filename) + raise InvalidLocaleData.new(filename) unless data.is_a?(Hash) + data.each { |locale, d| store_translations(locale, d || {}) } + end + + # Loads a plain Ruby translations file. eval'ing the file must yield + # a Hash containing translation data with locales as toplevel keys. + def load_rb(filename) + eval(IO.read(filename), binding, filename) + end + + # Loads a YAML translations file. The data must have locales as + # toplevel keys. + def load_yml(filename) + begin + YAML.load_file(filename) + rescue TypeError + nil + rescue SyntaxError + nil + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/cache.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/cache.rb new file mode 100644 index 0000000..3c456ff --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/cache.rb @@ -0,0 +1,96 @@ +# This module allows you to easily cache all responses from the backend - thus +# speeding up the I18n aspects of your application quite a bit. +# +# To enable caching you can simply include the Cache module to the Simple +# backend - or whatever other backend you are using: +# +# I18n::Backend::Simple.send(:include, I18n::Backend::Cache) +# +# You will also need to set a cache store implementation that you want to use: +# +# I18n.cache_store = ActiveSupport::Cache.lookup_store(:memory_store) +# +# You can use any cache implementation you want that provides the same API as +# ActiveSupport::Cache (only the methods #fetch and #write are being used). +# +# The cache_key implementation assumes that you only pass values to +# I18n.translate that return a valid key from #hash (see +# http://www.ruby-doc.org/core/classes/Object.html#M000337). +# +# If you use a lambda as a default value in your translation like this: +# +# I18n.t(:"date.order", :default => lambda {[:month, :day, :year]}) +# +# Then you will always have a cache miss, because each time this method +# is called the lambda will have a different hash value. If you know +# the result of the lambda is a constant as in the example above, then +# to cache this you can make the lambda a constant, like this: +# +# DEFAULT_DATE_ORDER = lambda {[:month, :day, :year]} +# ... +# I18n.t(:"date.order", :default => DEFAULT_DATE_ORDER) +# +# If the lambda may result in different values for each call then consider +# also using the Memoize backend. +# +module I18n + class << self + @@cache_store = nil + @@cache_namespace = nil + + def cache_store + @@cache_store + end + + def cache_store=(store) + @@cache_store = store + end + + def cache_namespace + @@cache_namespace + end + + def cache_namespace=(namespace) + @@cache_namespace = namespace + end + + def perform_caching? + !cache_store.nil? + end + end + + module Backend + # TODO Should the cache be cleared if new translations are stored? + module Cache + def translate(locale, key, options = {}) + I18n.perform_caching? ? fetch(cache_key(locale, key, options)) { super } : super + end + + protected + + def fetch(cache_key, &block) + result = _fetch(cache_key, &block) + throw(:exception, result) if result.is_a?(MissingTranslation) + result = result.dup if result.frozen? rescue result + result + end + + def _fetch(cache_key, &block) + result = I18n.cache_store.read(cache_key) and return result + result = catch(:exception, &block) + I18n.cache_store.write(cache_key, result) unless result.is_a?(Proc) + result + end + + def cache_key(locale, key, options) + # This assumes that only simple, native Ruby values are passed to I18n.translate. + "i18n/#{I18n.cache_namespace}/#{locale}/#{key.hash}/#{USE_INSPECT_HASH ? options.inspect.hash : options.hash}" + end + + private + # In Ruby < 1.9 the following is true: { :foo => 1, :bar => 2 }.hash == { :foo => 2, :bar => 1 }.hash + # Therefore we must use the hash of the inspect string instead to avoid cache key colisions. + USE_INSPECT_HASH = RUBY_VERSION <= "1.9" + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/cascade.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/cascade.rb new file mode 100644 index 0000000..d8fb1cf --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/cascade.rb @@ -0,0 +1,54 @@ +# The Cascade module adds the ability to do cascading lookups to backends that +# are compatible to the Simple backend. +# +# By cascading lookups we mean that for any key that can not be found the +# Cascade module strips one segment off the scope part of the key and then +# tries to look up the key in that scope. +# +# E.g. when a lookup for the key :"foo.bar.baz" does not yield a result then +# the segment :bar will be stripped off the scope part :"foo.bar" and the new +# scope :foo will be used to look up the key :baz. If that does not succeed +# then the remaining scope segment :foo will be omitted, too, and again the +# key :baz will be looked up (now with no scope). +# +# To enable a cascading lookup one passes the :cascade option: +# +# I18n.t(:'foo.bar.baz', :cascade => true) +# +# This will return the first translation found for :"foo.bar.baz", :"foo.baz" +# or :baz in this order. +# +# The cascading lookup takes precedence over resolving any given defaults. +# I.e. defaults will kick in after the cascading lookups haven't succeeded. +# +# This behavior is useful for libraries like ActiveRecord validations where +# the library wants to give users a bunch of more or less fine-grained options +# of scopes for a particular key. +# +# Thanks to Clemens Kofler for the initial idea and implementation! See +# http://github.com/clemens/i18n-cascading-backend + +module I18n + module Backend + module Cascade + def lookup(locale, key, scope = [], options = {}) + return super unless cascade = options[:cascade] + + cascade = { :step => 1 } unless cascade.is_a?(Hash) + step = cascade[:step] || 1 + offset = cascade[:offset] || 1 + separator = options[:separator] || I18n.default_separator + skip_root = cascade.has_key?(:skip_root) ? cascade[:skip_root] : true + + scope = I18n.normalize_keys(nil, key, scope, separator) + key = (scope.slice!(-offset, offset) || []).join(separator) + + begin + result = super + return result unless result.nil? + scope = scope.dup + end while (!scope.empty? || !skip_root) && scope.slice!(-step, step) + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/chain.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/chain.rb new file mode 100644 index 0000000..5a0c59b --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/chain.rb @@ -0,0 +1,78 @@ +module I18n + module Backend + # Backend that chains multiple other backends and checks each of them when + # a translation needs to be looked up. This is useful when you want to use + # standard translations with a Simple backend but store custom application + # translations in a database or other backends. + # + # To use the Chain backend instantiate it and set it to the I18n module. + # You can add chained backends through the initializer or backends + # accessor: + # + # # preserves the existing Simple backend set to I18n.backend + # I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend) + # + # The implementation assumes that all backends added to the Chain implement + # a lookup method with the same API as Simple backend does. + class Chain + module Implementation + include Base + + attr_accessor :backends + + def initialize(*backends) + self.backends = backends + end + + def reload! + backends.each { |backend| backend.reload! } + end + + def store_translations(locale, data, options = {}) + backends.first.store_translations(locale, data, options) + end + + def available_locales + backends.map { |backend| backend.available_locales }.flatten.uniq + end + + def translate(locale, key, default_options = {}) + namespace = nil + options = default_options.except(:default) + + backends.each do |backend| + catch(:exception) do + options = default_options if backend == backends.last + translation = backend.translate(locale, key, options) + if namespace_lookup?(translation, options) + namespace ||= {} + namespace.merge!(translation) + elsif !translation.nil? + return translation + end + end + end + + return namespace if namespace + throw(:exception, I18n::MissingTranslation.new(locale, key, options)) + end + + def localize(locale, object, format = :default, options = {}) + backends.each do |backend| + catch(:exception) do + result = backend.localize(locale, object, format, options) and return result + end + end + throw(:exception, I18n::MissingTranslation.new(locale, format, options)) + end + + protected + def namespace_lookup?(result, options) + result.is_a?(Hash) && !options.has_key?(:count) + end + end + + include Implementation + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/fallbacks.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/fallbacks.rb new file mode 100644 index 0000000..7252bb0 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/fallbacks.rb @@ -0,0 +1,65 @@ +# I18n locale fallbacks are useful when you want your application to use +# translations from other locales when translations for the current locale are +# missing. E.g. you might want to use :en translations when translations in +# your applications main locale :de are missing. +# +# To enable locale fallbacks you can simply include the Fallbacks module to +# the Simple backend - or whatever other backend you are using: +# +# I18n::Backend::Simple.include(I18n::Backend::Fallbacks) +module I18n + @@fallbacks = nil + + class << self + # Returns the current fallbacks implementation. Defaults to +I18n::Locale::Fallbacks+. + def fallbacks + @@fallbacks ||= I18n::Locale::Fallbacks.new + end + + # Sets the current fallbacks implementation. Use this to set a different fallbacks implementation. + def fallbacks=(fallbacks) + @@fallbacks = fallbacks + end + end + + module Backend + module Fallbacks + # Overwrites the Base backend translate method so that it will try each + # locale given by I18n.fallbacks for the given locale. E.g. for the + # locale :"de-DE" it might try the locales :"de-DE", :de and :en + # (depends on the fallbacks implementation) until it finds a result with + # the given options. If it does not find any result for any of the + # locales it will then throw MissingTranslation as usual. + # + # The default option takes precedence over fallback locales only when + # it's a Symbol. When the default contains a String, Proc or Hash + # it is evaluated last after all the fallback locales have been tried. + def translate(locale, key, options = {}) + return super if options[:fallback] + default = extract_non_symbol_default!(options) if options[:default] + + options[:fallback] = true + I18n.fallbacks[locale].each do |fallback| + catch(:exception) do + result = super(fallback, key, options) + return result unless result.nil? + end + end + options.delete(:fallback) + + return super(locale, nil, options.merge(:default => default)) if default + throw(:exception, I18n::MissingTranslation.new(locale, key, options)) + end + + def extract_non_symbol_default!(options) + defaults = [options[:default]].flatten + first_non_symbol_default = defaults.detect{|default| !default.is_a?(Symbol)} + if first_non_symbol_default + options[:default] = defaults[0, defaults.index(first_non_symbol_default)] + end + return first_non_symbol_default + end + + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/flatten.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/flatten.rb new file mode 100644 index 0000000..c23f7c1 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/flatten.rb @@ -0,0 +1,113 @@ +module I18n + module Backend + # This module contains several helpers to assist flattening translations. + # You may want to flatten translations for: + # + # 1) speed up lookups, as in the Memoize backend; + # 2) In case you want to store translations in a data store, as in ActiveRecord backend; + # + # You can check both backends above for some examples. + # This module also keeps all links in a hash so they can be properly resolved when flattened. + module Flatten + SEPARATOR_ESCAPE_CHAR = "\001" + FLATTEN_SEPARATOR = "." + + # normalize_keys the flatten way. This method is significantly faster + # and creates way less objects than the one at I18n.normalize_keys. + # It also handles escaping the translation keys. + def self.normalize_flat_keys(locale, key, scope, separator) + keys = [scope, key].flatten.compact + separator ||= I18n.default_separator + + if separator != FLATTEN_SEPARATOR + keys.map! do |k| + k.to_s.tr("#{FLATTEN_SEPARATOR}#{separator}", + "#{SEPARATOR_ESCAPE_CHAR}#{FLATTEN_SEPARATOR}") + end + end + + keys.join(".") + end + + # Receives a string and escape the default separator. + def self.escape_default_separator(key) #:nodoc: + key.to_s.tr(FLATTEN_SEPARATOR, SEPARATOR_ESCAPE_CHAR) + end + + # Shortcut to I18n::Backend::Flatten.normalize_flat_keys + # and then resolve_links. + def normalize_flat_keys(locale, key, scope, separator) + key = I18n::Backend::Flatten.normalize_flat_keys(locale, key, scope, separator) + resolve_link(locale, key) + end + + # Store flattened links. + def links + @links ||= Hash.new { |h,k| h[k] = {} } + end + + # Flatten keys for nested Hashes by chaining up keys: + # + # >> { "a" => { "b" => { "c" => "d", "e" => "f" }, "g" => "h" }, "i" => "j"}.wind + # => { "a.b.c" => "d", "a.b.e" => "f", "a.g" => "h", "i" => "j" } + # + def flatten_keys(hash, escape, prev_key=nil, &block) + hash.each_pair do |key, value| + key = escape_default_separator(key) if escape + curr_key = [prev_key, key].compact.join(FLATTEN_SEPARATOR).to_sym + yield curr_key, value + flatten_keys(value, escape, curr_key, &block) if value.is_a?(Hash) + end + end + + # Receives a hash of translations (where the key is a locale and + # the value is another hash) and return a hash with all + # translations flattened. + # + # Nested hashes are included in the flattened hash just if subtree + # is true and Symbols are automatically stored as links. + def flatten_translations(locale, data, escape, subtree) + hash = {} + flatten_keys(data, escape) do |key, value| + if value.is_a?(Hash) + hash[key] = value if subtree + else + store_link(locale, key, value) if value.is_a?(Symbol) + hash[key] = value + end + end + hash + end + + protected + + def store_link(locale, key, link) + links[locale.to_sym][key.to_s] = link.to_s + end + + def resolve_link(locale, key) + key, locale = key.to_s, locale.to_sym + links = self.links[locale] + + if links.key?(key) + links[key] + elsif link = find_link(locale, key) + store_link(locale, key, key.gsub(*link)) + else + key + end + end + + def find_link(locale, key) #:nodoc: + links[locale].each do |from, to| + return [from, to] if key[0, from.length] == from + end && nil + end + + def escape_default_separator(key) #:nodoc: + I18n::Backend::Flatten.escape_default_separator(key) + end + + end + end +end \ No newline at end of file diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/gettext.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/gettext.rb new file mode 100644 index 0000000..c357a6d --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/gettext.rb @@ -0,0 +1,72 @@ +require 'i18n/gettext' +require 'i18n/gettext/po_parser' + +# Experimental support for using Gettext po files to store translations. +# +# To use this you can simply include the module to the Simple backend - or +# whatever other backend you are using. +# +# I18n::Backend::Simple.include(I18n::Backend::Gettext) +# +# Now you should be able to include your Gettext translation (*.po) files to +# the I18n.load_path so they're loaded to the backend and you can use them as +# usual: +# +# I18n.load_path += Dir["path/to/locales/*.po"] +# +# Following the Gettext convention this implementation expects that your +# translation files are named by their locales. E.g. the file en.po would +# contain the translations for the English locale. +module I18n + module Backend + module Gettext + class PoData < Hash + def set_comment(msgid_or_sym, comment) + # ignore + end + end + + protected + def load_po(filename) + locale = ::File.basename(filename, '.po').to_sym + data = normalize(locale, parse(filename)) + { locale => data } + end + + def parse(filename) + GetText::PoParser.new.parse(::File.read(filename), PoData.new) + end + + def normalize(locale, data) + data.inject({}) do |result, (key, value)| + unless key.nil? || key.empty? + key = key.gsub(I18n::Gettext::CONTEXT_SEPARATOR, '|') + key, value = normalize_pluralization(locale, key, value) if key.index("\000") + + parts = key.split('|').reverse + normalized = parts.inject({}) do |_normalized, part| + { part => _normalized.empty? ? value : _normalized } + end + + result.deep_merge!(normalized) + end + result + end + end + + def normalize_pluralization(locale, key, value) + # FIXME po_parser includes \000 chars that can not be turned into Symbols + key = key.gsub("\000", I18n::Gettext::PLURAL_SEPARATOR).split(I18n::Gettext::PLURAL_SEPARATOR).first + + keys = I18n::Gettext.plural_keys(locale) + values = value.split("\000") + raise "invalid number of plurals: #{values.size}, keys: #{keys.inspect} on #{locale} locale for msgid #{key.inspect} with values #{values.inspect}" if values.size != keys.size + + result = {} + values.each_with_index { |_value, ix| result[keys[ix]] = _value } + [key, result] + end + + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/interpolation_compiler.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/interpolation_compiler.rb new file mode 100644 index 0000000..fc8a3a1 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/interpolation_compiler.rb @@ -0,0 +1,121 @@ +# The InterpolationCompiler module contains optimizations that can tremendously +# speed up the interpolation process on the Simple backend. +# +# It works by defining a pre-compiled method on stored translation Strings that +# already bring all the knowledge about contained interpolation variables etc. +# so that the actual recurring interpolation will be very fast. +# +# To enable pre-compiled interpolations you can simply include the +# InterpolationCompiler module to the Simple backend: +# +# I18n::Backend::Simple.include(I18n::Backend::InterpolationCompiler) +# +# Note that InterpolationCompiler does not yield meaningful results and consequently +# should not be used with Ruby 1.9 (YARV) but improves performance everywhere else +# (jRuby, Rubinius and 1.8.7). +module I18n + module Backend + module InterpolationCompiler + module Compiler + extend self + + TOKENIZER = /(%%\{[^\}]+\}|%\{[^\}]+\})/ + INTERPOLATION_SYNTAX_PATTERN = /(%)?(%\{([^\}]+)\})/ + + def compile_if_an_interpolation(string) + if interpolated_str?(string) + string.instance_eval <<-RUBY_EVAL, __FILE__, __LINE__ + def i18n_interpolate(v = {}) + "#{compiled_interpolation_body(string)}" + end + RUBY_EVAL + end + + string + end + + def interpolated_str?(str) + str.kind_of?(::String) && str =~ INTERPOLATION_SYNTAX_PATTERN + end + + protected + # tokenize("foo %{bar} baz %%{buz}") # => ["foo ", "%{bar}", " baz ", "%%{buz}"] + def tokenize(str) + str.split(TOKENIZER) + end + + def compiled_interpolation_body(str) + tokenize(str).map do |token| + (matchdata = token.match(INTERPOLATION_SYNTAX_PATTERN)) ? handle_interpolation_token(token, matchdata) : escape_plain_str(token) + end.join + end + + def handle_interpolation_token(interpolation, matchdata) + escaped, pattern, key = matchdata.values_at(1, 2, 3) + escaped ? pattern : compile_interpolation_token(key.to_sym) + end + + def compile_interpolation_token(key) + "\#{#{interpolate_or_raise_missing(key)}}" + end + + def interpolate_or_raise_missing(key) + escaped_key = escape_key_sym(key) + RESERVED_KEYS.include?(key) ? reserved_key(escaped_key) : interpolate_key(escaped_key) + end + + def interpolate_key(key) + [direct_key(key), nil_key(key), missing_key(key)].join('||') + end + + def direct_key(key) + "((t = v[#{key}]) && t.respond_to?(:call) ? t.call : t)" + end + + def nil_key(key) + "(v.has_key?(#{key}) && '')" + end + + def missing_key(key) + "raise(MissingInterpolationArgument.new(#{key}, self))" + end + + def reserved_key(key) + "raise(ReservedInterpolationKey.new(#{key}, self))" + end + + def escape_plain_str(str) + str.gsub(/"|\\|#/) {|x| "\\#{x}"} + end + + def escape_key_sym(key) + # rely on Ruby to do all the hard work :) + key.to_sym.inspect + end + end + + def interpolate(locale, string, values) + if string.respond_to?(:i18n_interpolate) + string.i18n_interpolate(values) + elsif values + super + else + string + end + end + + def store_translations(locale, data, options = {}) + compile_all_strings_in(data) + super + end + + protected + def compile_all_strings_in(data) + data.each_value do |value| + Compiler.compile_if_an_interpolation(value) + compile_all_strings_in(value) if value.kind_of?(Hash) + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/key_value.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/key_value.rb new file mode 100644 index 0000000..c34b797 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/key_value.rb @@ -0,0 +1,101 @@ +require 'i18n/backend/base' +require 'active_support/json' +require 'active_support/ordered_hash' # active_support/json/encoding uses ActiveSupport::OrderedHash but does not require it + +module I18n + module Backend + # This is a basic backend for key value stores. It receives on + # initialization the store, which should respond to three methods: + # + # * store#[](key) - Used to get a value + # * store#[]=(key, value) - Used to set a value + # * store#keys - Used to get all keys + # + # Since these stores only supports string, all values are converted + # to JSON before being stored, allowing it to also store booleans, + # hashes and arrays. However, this store does not support Procs. + # + # As the ActiveRecord backend, Symbols are just supported when loading + # translations from the filesystem or through explicit store translations. + # + # Also, avoid calling I18n.available_locales since it's a somehow + # expensive operation in most stores. + # + # == Example + # + # To setup I18n to use TokyoCabinet in memory is quite straightforward: + # + # require 'rufus/tokyo/cabinet' # gem install rufus-tokyo + # I18n.backend = I18n::Backend::KeyValue.new(Rufus::Tokyo::Cabinet.new('*')) + # + # == Performance + # + # You may make this backend even faster by including the Memoize module. + # However, notice that you should properly clear the cache if you change + # values directly in the key-store. + # + # == Subtrees + # + # In most backends, you are allowed to retrieve part of a translation tree: + # + # I18n.backend.store_translations :en, :foo => { :bar => :baz } + # I18n.t "foo" #=> { :bar => :baz } + # + # This backend supports this feature by default, but it slows down the storage + # of new data considerably and makes hard to delete entries. That said, you are + # allowed to disable the storage of subtrees on initialization: + # + # I18n::Backend::KeyValue.new(@store, false) + # + # This is useful if you are using a KeyValue backend chained to a Simple backend. + class KeyValue + module Implementation + attr_accessor :store + + include Base, Flatten + + def initialize(store, subtrees=true) + @store, @subtrees = store, subtrees + end + + def store_translations(locale, data, options = {}) + escape = options.fetch(:escape, true) + flatten_translations(locale, data, escape, @subtrees).each do |key, value| + key = "#{locale}.#{key}" + + case value + when Hash + if @subtrees && (old_value = @store[key]) + old_value = ActiveSupport::JSON.decode(old_value) + value = old_value.deep_symbolize_keys.deep_merge!(value) if old_value.is_a?(Hash) + end + when Proc + raise "Key-value stores cannot handle procs" + end + + @store[key] = ActiveSupport::JSON.encode([value]) unless value.is_a?(Symbol) + end + end + + def available_locales + locales = @store.keys.map { |k| k =~ /\./; $` } + locales.uniq! + locales.compact! + locales.map! { |k| k.to_sym } + locales + end + + protected + + def lookup(locale, key, scope = [], options = {}) + key = normalize_flat_keys(locale, key, scope, options[:separator]) + value = @store["#{locale}.#{key}"] + value = ActiveSupport::JSON.decode(value)[0] if value + value.is_a?(Hash) ? value.deep_symbolize_keys : value + end + end + + include Implementation + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/memoize.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/memoize.rb new file mode 100644 index 0000000..ae9801f --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/memoize.rb @@ -0,0 +1,46 @@ +# Memoize module simply memoizes the values returned by lookup using +# a flat hash and can tremendously speed up the lookup process in a backend. +# +# To enable it you can simply include the Memoize module to your backend: +# +# I18n::Backend::Simple.include(I18n::Backend::Memoize) +# +# Notice that it's the responsibility of the backend to define whenever the +# cache should be cleaned. +module I18n + module Backend + module Memoize + def available_locales + @memoized_locales ||= super + end + + def store_translations(locale, data, options = {}) + reset_memoizations!(locale) + super + end + + def reload! + reset_memoizations! + super + end + + protected + + def lookup(locale, key, scope = nil, options = {}) + flat_key = I18n::Backend::Flatten.normalize_flat_keys(locale, + key, scope, options[:separator]).to_sym + flat_hash = memoized_lookup[locale.to_sym] + flat_hash.key?(flat_key) ? flat_hash[flat_key] : (flat_hash[flat_key] = super) + end + + def memoized_lookup + @memoized_lookup ||= Hash.new { |h, k| h[k] = {} } + end + + def reset_memoizations!(locale=nil) + @memoized_locales = nil + (locale ? memoized_lookup[locale.to_sym] : memoized_lookup).clear + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/metadata.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/metadata.rb new file mode 100644 index 0000000..52c0a29 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/metadata.rb @@ -0,0 +1,65 @@ +# I18n translation metadata is useful when you want to access information +# about how a translation was looked up, pluralized or interpolated in +# your application. +# +# msg = I18n.t(:message, :default => 'Hi!', :scope => :foo) +# msg.translation_metadata +# # => { :key => :message, :scope => :foo, :default => 'Hi!' } +# +# If a :count option was passed to #translate it will be set to the metadata. +# Likewise, if any interpolation variables were passed they will also be set. +# +# To enable translation metadata you can simply include the Metadata module +# into the Simple backend class - or whatever other backend you are using: +# +# I18n::Backend::Simple.include(I18n::Backend::Metadata) +# +module I18n + module Backend + module Metadata + class << self + def included(base) + Object.class_eval do + def translation_metadata + @translation_metadata ||= {} + end + + def translation_metadata=(translation_metadata) + @translation_metadata = translation_metadata + end + end unless Object.method_defined?(:translation_metadata) + end + end + + def translate(locale, key, options = {}) + metadata = { + :locale => locale, + :key => key, + :scope => options[:scope], + :default => options[:default], + :separator => options[:separator], + :values => options.reject { |name, value| RESERVED_KEYS.include?(name) } + } + with_metadata(metadata) { super } + end + + def interpolate(locale, entry, values = {}) + metadata = entry.translation_metadata.merge(:original => entry) + with_metadata(metadata) { super } + end + + def pluralize(locale, entry, count) + with_metadata(:count => count) { super } + end + + protected + + def with_metadata(metadata, &block) + result = yield + result.translation_metadata = result.translation_metadata.merge(metadata) if result + result + end + + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/pluralization.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/pluralization.rb new file mode 100644 index 0000000..c73a009 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/pluralization.rb @@ -0,0 +1,53 @@ +# I18n Pluralization are useful when you want your application to +# customize pluralization rules. +# +# To enable locale specific pluralizations you can simply include the +# Pluralization module to the Simple backend - or whatever other backend you +# are using. +# +# I18n::Backend::Simple.include(I18n::Backend::Pluralization) +# +# You also need to make sure to provide pluralization algorithms to the +# backend, i.e. include them to your I18n.load_path accordingly. +module I18n + module Backend + module Pluralization + # Overwrites the Base backend translate method so that it will check the + # translation meta data space (:i18n) for a locale specific pluralization + # rule and use it to pluralize the given entry. I.e. the library expects + # pluralization rules to be stored at I18n.t(:'i18n.plural.rule') + # + # Pluralization rules are expected to respond to #call(count) and + # return a pluralization key. Valid keys depend on the translation data + # hash (entry) but it is generally recommended to follow CLDR's style, + # i.e., return one of the keys :zero, :one, :few, :many, :other. + # + # The :zero key is always picked directly when count equals 0 AND the + # translation data has the key :zero. This way translators are free to + # either pick a special :zero translation even for languages where the + # pluralizer does not return a :zero key. + def pluralize(locale, entry, count) + return entry unless entry.is_a?(Hash) and count + + pluralizer = pluralizer(locale) + if pluralizer.respond_to?(:call) + key = count == 0 && entry.has_key?(:zero) ? :zero : pluralizer.call(count) + raise InvalidPluralizationData.new(entry, count) unless entry.has_key?(key) + entry[key] + else + super + end + end + + protected + + def pluralizers + @pluralizers ||= {} + end + + def pluralizer(locale) + pluralizers[locale] ||= I18n.t(:'i18n.plural.rule', :locale => locale, :resolve => false) + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/simple.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/simple.rb new file mode 100644 index 0000000..95ffb6a --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/simple.rb @@ -0,0 +1,87 @@ +module I18n + module Backend + # A simple backend that reads translations from YAML files and stores them in + # an in-memory hash. Relies on the Base backend. + # + # The implementation is provided by a Implementation module allowing to easily + # extend Simple backend's behavior by including modules. E.g.: + # + # module I18n::Backend::Pluralization + # def pluralize(*args) + # # extended pluralization logic + # super + # end + # end + # + # I18n::Backend::Simple.include(I18n::Backend::Pluralization) + class Simple + (class << self; self; end).class_eval { public :include } + + module Implementation + include Base + + def initialized? + @initialized ||= false + end + + # Stores translations for the given locale in memory. + # This uses a deep merge for the translations hash, so existing + # translations will be overwritten by new ones only at the deepest + # level of the hash. + def store_translations(locale, data, options = {}) + locale = locale.to_sym + translations[locale] ||= {} + data = data.deep_symbolize_keys + translations[locale].deep_merge!(data) + end + + # Get available locales from the translations hash + def available_locales + init_translations unless initialized? + translations.inject([]) do |locales, (locale, data)| + locales << locale unless (data.keys - [:i18n]).empty? + locales + end + end + + # Clean up translations hash and set initialized to false on reload! + def reload! + @initialized = false + @translations = nil + super + end + + protected + + def init_translations + load_translations + @initialized = true + end + + def translations + @translations ||= {} + end + + # Looks up a translation from the translations hash. Returns nil if + # eiher key is nil, or locale, scope or key do not exist as a key in the + # nested translations hash. Splits keys or scopes containing dots + # into multiple keys, i.e. currency.format is regarded the same as + # %w(currency format). + def lookup(locale, key, scope = [], options = {}) + init_translations unless initialized? + keys = I18n.normalize_keys(locale, key, scope, options[:separator]) + + keys.inject(translations) do |result, _key| + _key = _key.to_sym + return nil unless result.is_a?(Hash) && result.has_key?(_key) + result = result[_key] + result = resolve(locale, _key, result, options.merge(:scope => nil)) if result.is_a?(Symbol) + result + end + end + end + + include Implementation + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/backend/transliterator.rb b/lib/mcollective/vendor/i18n/lib/i18n/backend/transliterator.rb new file mode 100644 index 0000000..2ce2cc8 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/backend/transliterator.rb @@ -0,0 +1,98 @@ +# encoding: utf-8 +module I18n + module Backend + module Transliterator + DEFAULT_REPLACEMENT_CHAR = "?" + + # Given a locale and a UTF-8 string, return the locale's ASCII + # approximation for the string. + def transliterate(locale, string, replacement = nil) + @transliterators ||= {} + @transliterators[locale] ||= Transliterator.get I18n.t(:'i18n.transliterate.rule', + :locale => locale, :resolve => false, :default => {}) + @transliterators[locale].transliterate(string, replacement) + end + + # Get a transliterator instance. + def self.get(rule = nil) + if !rule || rule.kind_of?(Hash) + HashTransliterator.new(rule) + elsif rule.kind_of? Proc + ProcTransliterator.new(rule) + else + raise I18n::ArgumentError, "Transliteration rule must be a proc or a hash." + end + end + + # A transliterator which accepts a Proc as its transliteration rule. + class ProcTransliterator + def initialize(rule) + @rule = rule + end + + def transliterate(string, replacement = nil) + @rule.call(string) + end + end + + # A transliterator which accepts a Hash of characters as its translation + # rule. + class HashTransliterator + DEFAULT_APPROXIMATIONS = { + "À"=>"A", "Á"=>"A", "Â"=>"A", "Ã"=>"A", "Ä"=>"A", "Å"=>"A", "Æ"=>"AE", + "Ç"=>"C", "È"=>"E", "É"=>"E", "Ê"=>"E", "Ë"=>"E", "Ì"=>"I", "Í"=>"I", + "Î"=>"I", "Ï"=>"I", "Ð"=>"D", "Ñ"=>"N", "Ò"=>"O", "Ó"=>"O", "Ô"=>"O", + "Õ"=>"O", "Ö"=>"O", "×"=>"x", "Ø"=>"O", "Ù"=>"U", "Ú"=>"U", "Û"=>"U", + "Ü"=>"U", "Ý"=>"Y", "Þ"=>"Th", "ß"=>"ss", "à"=>"a", "á"=>"a", "â"=>"a", + "ã"=>"a", "ä"=>"a", "å"=>"a", "æ"=>"ae", "ç"=>"c", "è"=>"e", "é"=>"e", + "ê"=>"e", "ë"=>"e", "ì"=>"i", "í"=>"i", "î"=>"i", "ï"=>"i", "ð"=>"d", + "ñ"=>"n", "ò"=>"o", "ó"=>"o", "ô"=>"o", "õ"=>"o", "ö"=>"o", "ø"=>"o", + "ù"=>"u", "ú"=>"u", "û"=>"u", "ü"=>"u", "ý"=>"y", "þ"=>"th", "ÿ"=>"y", + "Ā"=>"A", "ā"=>"a", "Ă"=>"A", "ă"=>"a", "Ą"=>"A", "ą"=>"a", "Ć"=>"C", + "ć"=>"c", "Ĉ"=>"C", "ĉ"=>"c", "Ċ"=>"C", "ċ"=>"c", "Č"=>"C", "č"=>"c", + "Ď"=>"D", "ď"=>"d", "Đ"=>"D", "đ"=>"d", "Ē"=>"E", "ē"=>"e", "Ĕ"=>"E", + "ĕ"=>"e", "Ė"=>"E", "ė"=>"e", "Ę"=>"E", "ę"=>"e", "Ě"=>"E", "ě"=>"e", + "Ĝ"=>"G", "ĝ"=>"g", "Ğ"=>"G", "ğ"=>"g", "Ġ"=>"G", "ġ"=>"g", "Ģ"=>"G", + "ģ"=>"g", "Ĥ"=>"H", "ĥ"=>"h", "Ħ"=>"H", "ħ"=>"h", "Ĩ"=>"I", "ĩ"=>"i", + "Ī"=>"I", "ī"=>"i", "Ĭ"=>"I", "ĭ"=>"i", "Į"=>"I", "į"=>"i", "İ"=>"I", + "ı"=>"i", "IJ"=>"IJ", "ij"=>"ij", "Ĵ"=>"J", "ĵ"=>"j", "Ķ"=>"K", "ķ"=>"k", + "ĸ"=>"k", "Ĺ"=>"L", "ĺ"=>"l", "Ļ"=>"L", "ļ"=>"l", "Ľ"=>"L", "ľ"=>"l", + "Ŀ"=>"L", "ŀ"=>"l", "Ł"=>"L", "ł"=>"l", "Ń"=>"N", "ń"=>"n", "Ņ"=>"N", + "ņ"=>"n", "Ň"=>"N", "ň"=>"n", "ʼn"=>"'n", "Ŋ"=>"NG", "ŋ"=>"ng", + "Ō"=>"O", "ō"=>"o", "Ŏ"=>"O", "ŏ"=>"o", "Ő"=>"O", "ő"=>"o", "Œ"=>"OE", + "œ"=>"oe", "Ŕ"=>"R", "ŕ"=>"r", "Ŗ"=>"R", "ŗ"=>"r", "Ř"=>"R", "ř"=>"r", + "Ś"=>"S", "ś"=>"s", "Ŝ"=>"S", "ŝ"=>"s", "Ş"=>"S", "ş"=>"s", "Š"=>"S", + "š"=>"s", "Ţ"=>"T", "ţ"=>"t", "Ť"=>"T", "ť"=>"t", "Ŧ"=>"T", "ŧ"=>"t", + "Ũ"=>"U", "ũ"=>"u", "Ū"=>"U", "ū"=>"u", "Ŭ"=>"U", "ŭ"=>"u", "Ů"=>"U", + "ů"=>"u", "Ű"=>"U", "ű"=>"u", "Ų"=>"U", "ų"=>"u", "Ŵ"=>"W", "ŵ"=>"w", + "Ŷ"=>"Y", "ŷ"=>"y", "Ÿ"=>"Y", "Ź"=>"Z", "ź"=>"z", "Ż"=>"Z", "ż"=>"z", + "Ž"=>"Z", "ž"=>"z" + } + + def initialize(rule = nil) + @rule = rule + add DEFAULT_APPROXIMATIONS + add rule if rule + end + + def transliterate(string, replacement = nil) + string.gsub(/[^\x00-\x7f]/u) do |char| + approximations[char] || replacement || DEFAULT_REPLACEMENT_CHAR + end + end + + private + + def approximations + @approximations ||= {} + end + + # Add transliteration rules to the approximations hash. + def add(hash) + hash.keys.each {|key| hash[key.to_s] = hash.delete(key).to_s} + approximations.merge! hash + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/config.rb b/lib/mcollective/vendor/i18n/lib/i18n/config.rb new file mode 100644 index 0000000..5fe05f7 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/config.rb @@ -0,0 +1,86 @@ +module I18n + class Config + # The only configuration value that is not global and scoped to thread is :locale. + # It defaults to the default_locale. + def locale + @locale ||= default_locale + end + + # Sets the current locale pseudo-globally, i.e. in the Thread.current hash. + def locale=(locale) + @locale = locale.to_sym rescue nil + end + + # Returns the current backend. Defaults to +Backend::Simple+. + def backend + @@backend ||= Backend::Simple.new + end + + # Sets the current backend. Used to set a custom backend. + def backend=(backend) + @@backend = backend + end + + # Returns the current default locale. Defaults to :'en' + def default_locale + @@default_locale ||= :en + end + + # Sets the current default locale. Used to set a custom default locale. + def default_locale=(locale) + @@default_locale = locale.to_sym rescue nil + end + + # Returns an array of locales for which translations are available. + # Unless you explicitely set these through I18n.available_locales= + # the call will be delegated to the backend. + def available_locales + @@available_locales ||= nil + @@available_locales || backend.available_locales + end + + # Sets the available locales. + def available_locales=(locales) + @@available_locales = Array(locales).map { |locale| locale.to_sym } + @@available_locales = nil if @@available_locales.empty? + end + + # Returns the current default scope separator. Defaults to '.' + def default_separator + @@default_separator ||= '.' + end + + # Sets the current default scope separator. + def default_separator=(separator) + @@default_separator = separator + end + + # Return the current exception handler. Defaults to :default_exception_handler. + def exception_handler + @@exception_handler ||= ExceptionHandler.new + end + + # Sets the exception handler. + def exception_handler=(exception_handler) + @@exception_handler = exception_handler + end + + # Allow clients to register paths providing translation data sources. The + # backend defines acceptable sources. + # + # E.g. the provided SimpleBackend accepts a list of paths to translation + # files which are either named *.rb and contain plain Ruby Hashes or are + # named *.yml and contain YAML data. So for the SimpleBackend clients may + # register translation files like this: + # I18n.load_path << 'path/to/locale/en.yml' + def load_path + @@load_path ||= [] + end + + # Sets the load path instance. Custom implementations are expected to + # behave like a Ruby Array. + def load_path=(load_path) + @@load_path = load_path + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/core_ext/hash.rb b/lib/mcollective/vendor/i18n/lib/i18n/core_ext/hash.rb new file mode 100644 index 0000000..f2a2422 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/core_ext/hash.rb @@ -0,0 +1,29 @@ +class Hash + def slice(*keep_keys) + h = {} + keep_keys.each { |key| h[key] = fetch(key) } + h + end unless Hash.method_defined?(:slice) + + def except(*less_keys) + slice(*keys - less_keys) + end unless Hash.method_defined?(:except) + + def deep_symbolize_keys + inject({}) { |result, (key, value)| + value = value.deep_symbolize_keys if value.is_a?(Hash) + result[(key.to_sym rescue key) || key] = value + result + } + end unless Hash.method_defined?(:deep_symbolize_keys) + + # deep_merge_hash! by Stefan Rusterholz, see http://www.ruby-forum.com/topic/142809 + MERGER = proc do |key, v1, v2| + Hash === v1 && Hash === v2 ? v1.merge(v2, &MERGER) : v2 + end + + def deep_merge!(data) + merge!(data, &MERGER) + end unless Hash.method_defined?(:deep_merge!) +end + diff --git a/lib/mcollective/vendor/i18n/lib/i18n/core_ext/kernel/surpress_warnings.rb b/lib/mcollective/vendor/i18n/lib/i18n/core_ext/kernel/surpress_warnings.rb new file mode 100644 index 0000000..cc03b1c --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/core_ext/kernel/surpress_warnings.rb @@ -0,0 +1,9 @@ +module Kernel + def suppress_warnings + original_verbosity = $VERBOSE + $VERBOSE = nil + result = yield + $VERBOSE = original_verbosity + result + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/core_ext/string/interpolate.rb b/lib/mcollective/vendor/i18n/lib/i18n/core_ext/string/interpolate.rb new file mode 100644 index 0000000..56de8c0 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/core_ext/string/interpolate.rb @@ -0,0 +1,105 @@ +# This backports the Ruby 1.9 String interpolation syntax to Ruby 1.8. +# +# This backport has been shipped with I18n for a number of versions. Meanwhile +# Rails has started to rely on it and we are going to move it to ActiveSupport. +# See https://rails.lighthouseapp.com/projects/8994/tickets/6013-move-19-string-interpolation-syntax-backport-from-i18n-to-activesupport +# +# Once the above patch has been applied to Rails the following code will be +# removed from I18n. + +=begin + heavily based on Masao Mutoh's gettext String interpolation extension + http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb + Copyright (C) 2005-2009 Masao Mutoh + You may redistribute it and/or modify it under the same license terms as Ruby. +=end + +begin + raise ArgumentError if ("a %{x}" % {:x=>'b'}) != 'a b' +rescue ArgumentError + # KeyError is raised by String#% when the string contains a named placeholder + # that is not contained in the given arguments hash. Ruby 1.9 includes and + # raises this exception natively. We define it to mimic Ruby 1.9's behaviour + # in Ruby 1.8.x + class KeyError < IndexError + def initialize(message = nil) + super(message || "key not found") + end + end unless defined?(KeyError) + + # Extension for String class. This feature is included in Ruby 1.9 or later but not occur TypeError. + # + # String#% method which accept "named argument". The translator can know + # the meaning of the msgids using "named argument" instead of %s/%d style. + class String + # For older ruby versions, such as ruby-1.8.5 + alias :bytesize :size unless instance_methods.find {|m| m.to_s == 'bytesize'} + alias :interpolate_without_ruby_19_syntax :% # :nodoc: + + INTERPOLATION_PATTERN = Regexp.union( + /%\{(\w+)\}/, # matches placeholders like "%{foo}" + /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%.d" + ) + + INTERPOLATION_PATTERN_WITH_ESCAPE = Regexp.union( + /%%/, + INTERPOLATION_PATTERN + ) + + # % uses self (i.e. the String) as a format specification and returns the + # result of applying it to the given arguments. In other words it interpolates + # the given arguments to the string according to the formats the string + # defines. + # + # There are three ways to use it: + # + # * Using a single argument or Array of arguments. + # + # This is the default behaviour of the String class. See Kernel#sprintf for + # more details about the format string. + # + # Example: + # + # "%d %s" % [1, "message"] + # # => "1 message" + # + # * Using a Hash as an argument and unformatted, named placeholders. + # + # When you pass a Hash as an argument and specify placeholders with %{foo} + # it will interpret the hash values as named arguments. + # + # Example: + # + # "%{firstname}, %{lastname}" % {:firstname => "Masao", :lastname => "Mutoh"} + # # => "Masao Mutoh" + # + # * Using a Hash as an argument and formatted, named placeholders. + # + # When you pass a Hash as an argument and specify placeholders with %d + # it will interpret the hash values as named arguments and format the value + # according to the formatting instruction appended to the closing >. + # + # Example: + # + # "%d, %.1f" % { :integer => 10, :float => 43.4 } + # # => "10, 43.3" + def %(args) + if args.kind_of?(Hash) + dup.gsub(INTERPOLATION_PATTERN_WITH_ESCAPE) do |match| + if match == '%%' + '%' + else + key = ($1 || $2).to_sym + raise KeyError unless args.has_key?(key) + $3 ? sprintf("%#{$3}", args[key]) : args[key] + end + end + elsif self =~ INTERPOLATION_PATTERN + raise ArgumentError.new('one hash required') + else + result = gsub(/%([{<])/, '%%\1') + result.send :'interpolate_without_ruby_19_syntax', args + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/exceptions.rb b/lib/mcollective/vendor/i18n/lib/i18n/exceptions.rb new file mode 100644 index 0000000..2f625a0 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/exceptions.rb @@ -0,0 +1,106 @@ +module I18n + # Handles exceptions raised in the backend. All exceptions except for + # MissingTranslationData exceptions are re-thrown. When a MissingTranslationData + # was caught the handler returns an error message string containing the key/scope. + # Note that the exception handler is not called when the option :throw was given. + class ExceptionHandler + include Module.new { + def call(exception, locale, key, options) + if exception.is_a?(MissingTranslation) + options[:rescue_format] == :html ? exception.html_message : exception.message + elsif exception.is_a?(Exception) + raise exception + else + throw :exception, exception + end + end + } + end + + class ArgumentError < ::ArgumentError; end + + class InvalidLocale < ArgumentError + attr_reader :locale + def initialize(locale) + @locale = locale + super "#{locale.inspect} is not a valid locale" + end + end + + class InvalidLocaleData < ArgumentError + attr_reader :filename + def initialize(filename) + @filename = filename + super "can not load translations from #{filename}, expected it to return a hash, but does not" + end + end + + class MissingTranslation + module Base + attr_reader :locale, :key, :options + + def initialize(locale, key, options = nil) + @key, @locale, @options = key, locale, options.dup || {} + options.each { |k, v| self.options[k] = v.inspect if v.is_a?(Proc) } + end + + def html_message + key = keys.last.to_s.gsub('_', ' ').gsub(/\b('?[a-z])/) { $1.capitalize } + %(#{key}) + end + + def keys + @keys ||= I18n.normalize_keys(locale, key, options[:scope]).tap do |keys| + keys << 'no key' if keys.size < 2 + end + end + + def message + "translation missing: #{keys.join('.')}" + end + alias :to_s :message + + def to_exception + MissingTranslationData.new(locale, key, options) + end + end + + include Base + end + + class MissingTranslationData < ArgumentError + include MissingTranslation::Base + end + + class InvalidPluralizationData < ArgumentError + attr_reader :entry, :count + def initialize(entry, count) + @entry, @count = entry, count + super "translation data #{entry.inspect} can not be used with :count => #{count}" + end + end + + class MissingInterpolationArgument < ArgumentError + attr_reader :values, :string + def initialize(values, string) + @values, @string = values, string + super "missing interpolation argument in #{string.inspect} (#{values.inspect} given)" + end + end + + class ReservedInterpolationKey < ArgumentError + attr_reader :key, :string + def initialize(key, string) + @key, @string = key, string + super "reserved key #{key.inspect} used in #{string.inspect}" + end + end + + class UnknownFileType < ArgumentError + attr_reader :type, :filename + def initialize(type, filename) + @type, @filename = type, filename + super "can not load translations from #{filename}, the file type #{type} is not known" + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/gettext.rb b/lib/mcollective/vendor/i18n/lib/i18n/gettext.rb new file mode 100644 index 0000000..26a5d48 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/gettext.rb @@ -0,0 +1,25 @@ +module I18n + module Gettext + PLURAL_SEPARATOR = "\001" + CONTEXT_SEPARATOR = "\004" + + autoload :Helpers, 'i18n/gettext/helpers' + + @@plural_keys = { :en => [:one, :other] } + + class << self + # returns an array of plural keys for the given locale so that we can + # convert from gettext's integer-index based style + # TODO move this information to the pluralization module + def plural_keys(locale) + @@plural_keys[locale] || @@plural_keys[:en] + end + + def extract_scope(msgid, separator) + scope = msgid.to_s.split(separator) + msgid = scope.pop + [scope, msgid] + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/gettext/helpers.rb b/lib/mcollective/vendor/i18n/lib/i18n/gettext/helpers.rb new file mode 100644 index 0000000..ea07d05 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/gettext/helpers.rb @@ -0,0 +1,64 @@ +require 'i18n/gettext' + +module I18n + module Gettext + # Implements classical Gettext style accessors. To use this include the + # module to the global namespace or wherever you want to use it. + # + # include I18n::Gettext::Helpers + module Helpers + def gettext(msgid, options = {}) + I18n.t(msgid, { :default => msgid, :separator => '|' }.merge(options)) + end + alias _ gettext + + def sgettext(msgid, separator = '|') + scope, msgid = I18n::Gettext.extract_scope(msgid, separator) + I18n.t(msgid, :scope => scope, :default => msgid, :separator => separator) + end + alias s_ sgettext + + def pgettext(msgctxt, msgid) + separator = I18n::Gettext::CONTEXT_SEPARATOR + sgettext([msgctxt, msgid].join(separator), separator) + end + alias p_ pgettext + + def ngettext(msgid, msgid_plural, n = 1) + nsgettext(msgid, msgid_plural, n) + end + alias n_ ngettext + + # Method signatures: + # nsgettext('Fruits|apple', 'apples', 2) + # nsgettext(['Fruits|apple', 'apples'], 2) + def nsgettext(msgid, msgid_plural, n = 1, separator = '|') + if msgid.is_a?(Array) + msgid, msgid_plural, n, separator = msgid[0], msgid[1], msgid_plural, n + separator = '|' unless separator.is_a?(::String) + end + + scope, msgid = I18n::Gettext.extract_scope(msgid, separator) + default = { :one => msgid, :other => msgid_plural } + I18n.t(msgid, :default => default, :count => n, :scope => scope, :separator => separator) + end + alias ns_ nsgettext + + # Method signatures: + # npgettext('Fruits', 'apple', 'apples', 2) + # npgettext('Fruits', ['apple', 'apples'], 2) + def npgettext(msgctxt, msgid, msgid_plural, n = 1) + separator = I18n::Gettext::CONTEXT_SEPARATOR + + if msgid.is_a?(Array) + msgid_plural, msgid, n = msgid[1], [msgctxt, msgid[0]].join(separator), msgid_plural + else + msgid = [msgctxt, msgid].join(separator) + end + + nsgettext(msgid, msgid_plural, n, separator) + end + alias np_ npgettext + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/gettext/po_parser.rb b/lib/mcollective/vendor/i18n/lib/i18n/gettext/po_parser.rb new file mode 100644 index 0000000..547df6a --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/gettext/po_parser.rb @@ -0,0 +1,329 @@ +=begin + poparser.rb - Generate a .mo + + Copyright (C) 2003-2009 Masao Mutoh + + You may redistribute it and/or modify it under the same + license terms as Ruby. +=end + +#MODIFIED +# removed include GetText etc +# added stub translation method _(x) +require 'racc/parser' + +module GetText + + class PoParser < Racc::Parser + + def _(x) + x + end + +module_eval <<'..end src/poparser.ry modeval..id7a99570e05', 'src/poparser.ry', 108 + def unescape(orig) + ret = orig.gsub(/\\n/, "\n") + ret.gsub!(/\\t/, "\t") + ret.gsub!(/\\r/, "\r") + ret.gsub!(/\\"/, "\"") + ret + end + + def parse(str, data, ignore_fuzzy = true) + @comments = [] + @data = data + @fuzzy = false + @msgctxt = "" + $ignore_fuzzy = ignore_fuzzy + + str.strip! + @q = [] + until str.empty? do + case str + when /\A\s+/ + str = $' + when /\Amsgctxt/ + @q.push [:MSGCTXT, $&] + str = $' + when /\Amsgid_plural/ + @q.push [:MSGID_PLURAL, $&] + str = $' + when /\Amsgid/ + @q.push [:MSGID, $&] + str = $' + when /\Amsgstr/ + @q.push [:MSGSTR, $&] + str = $' + when /\A\[(\d+)\]/ + @q.push [:PLURAL_NUM, $1] + str = $' + when /\A\#~(.*)/ + $stderr.print _("Warning: obsolete msgid exists.\n") + $stderr.print " #{$&}\n" + @q.push [:COMMENT, $&] + str = $' + when /\A\#(.*)/ + @q.push [:COMMENT, $&] + str = $' + when /\A\"(.*)\"/ + @q.push [:STRING, $1] + str = $' + else + #c = str[0,1] + #@q.push [:STRING, c] + str = str[1..-1] + end + end + @q.push [false, '$end'] + if $DEBUG + @q.each do |a,b| + puts "[#{a}, #{b}]" + end + end + @yydebug = true if $DEBUG + do_parse + + if @comments.size > 0 + @data.set_comment(:last, @comments.join("\n")) + end + @data + end + + def next_token + @q.shift + end + + def on_message(msgid, msgstr) + if msgstr.size > 0 + @data[msgid] = msgstr + @data.set_comment(msgid, @comments.join("\n")) + end + @comments.clear + @msgctxt = "" + end + + def on_comment(comment) + @fuzzy = true if (/fuzzy/ =~ comment) + @comments << comment + end + + +..end src/poparser.ry modeval..id7a99570e05 + +##### racc 1.4.5 generates ### + +racc_reduce_table = [ + 0, 0, :racc_error, + 0, 10, :_reduce_none, + 2, 10, :_reduce_none, + 2, 10, :_reduce_none, + 2, 10, :_reduce_none, + 2, 12, :_reduce_5, + 1, 13, :_reduce_none, + 1, 13, :_reduce_none, + 4, 15, :_reduce_8, + 5, 16, :_reduce_9, + 2, 17, :_reduce_10, + 1, 17, :_reduce_none, + 3, 18, :_reduce_12, + 1, 11, :_reduce_13, + 2, 14, :_reduce_14, + 1, 14, :_reduce_15 ] + +racc_reduce_n = 16 + +racc_shift_n = 26 + +racc_action_table = [ + 3, 13, 5, 7, 9, 15, 16, 17, 20, 17, + 13, 17, 13, 13, 11, 17, 23, 20, 13, 17 ] + +racc_action_check = [ + 1, 16, 1, 1, 1, 12, 12, 12, 18, 18, + 7, 14, 15, 9, 3, 19, 20, 21, 23, 25 ] + +racc_action_pointer = [ + nil, 0, nil, 14, nil, nil, nil, 3, nil, 6, + nil, nil, 0, nil, 4, 5, -6, nil, 2, 8, + 8, 11, nil, 11, nil, 12 ] + +racc_action_default = [ + -1, -16, -2, -16, -3, -13, -4, -16, -6, -16, + -7, 26, -16, -15, -5, -16, -16, -14, -16, -8, + -16, -9, -11, -16, -10, -12 ] + +racc_goto_table = [ + 12, 22, 14, 4, 24, 6, 2, 8, 18, 19, + 10, 21, 1, nil, nil, nil, 25 ] + +racc_goto_check = [ + 5, 9, 5, 3, 9, 4, 2, 6, 5, 5, + 7, 8, 1, nil, nil, nil, 5 ] + +racc_goto_pointer = [ + nil, 12, 5, 2, 4, -7, 6, 9, -7, -17 ] + +racc_goto_default = [ + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil ] + +racc_token_table = { + false => 0, + Object.new => 1, + :COMMENT => 2, + :MSGID => 3, + :MSGCTXT => 4, + :MSGID_PLURAL => 5, + :MSGSTR => 6, + :STRING => 7, + :PLURAL_NUM => 8 } + +racc_use_result_var = true + +racc_nt_base = 9 + +Racc_arg = [ + racc_action_table, + racc_action_check, + racc_action_default, + racc_action_pointer, + racc_goto_table, + racc_goto_check, + racc_goto_default, + racc_goto_pointer, + racc_nt_base, + racc_reduce_table, + racc_token_table, + racc_shift_n, + racc_reduce_n, + racc_use_result_var ] + +Racc_token_to_s_table = [ +'$end', +'error', +'COMMENT', +'MSGID', +'MSGCTXT', +'MSGID_PLURAL', +'MSGSTR', +'STRING', +'PLURAL_NUM', +'$start', +'msgfmt', +'comment', +'msgctxt', +'message', +'string_list', +'single_message', +'plural_message', +'msgstr_plural', +'msgstr_plural_line'] + +Racc_debug_parser = true + +##### racc system variables end ##### + + # reduce 0 omitted + + # reduce 1 omitted + + # reduce 2 omitted + + # reduce 3 omitted + + # reduce 4 omitted + +module_eval <<'.,.,', 'src/poparser.ry', 25 + def _reduce_5( val, _values, result ) + @msgctxt = unescape(val[1]) + "\004" + result + end +.,., + + # reduce 6 omitted + + # reduce 7 omitted + +module_eval <<'.,.,', 'src/poparser.ry', 48 + def _reduce_8( val, _values, result ) + if @fuzzy and $ignore_fuzzy + if val[1] != "" + $stderr.print _("Warning: fuzzy message was ignored.\n") + $stderr.print " msgid '#{val[1]}'\n" + else + on_message('', unescape(val[3])) + end + @fuzzy = false + else + on_message(@msgctxt + unescape(val[1]), unescape(val[3])) + end + result = "" + result + end +.,., + +module_eval <<'.,.,', 'src/poparser.ry', 65 + def _reduce_9( val, _values, result ) + if @fuzzy and $ignore_fuzzy + if val[1] != "" + $stderr.print _("Warning: fuzzy message was ignored.\n") + $stderr.print "msgid = '#{val[1]}\n" + else + on_message('', unescape(val[3])) + end + @fuzzy = false + else + on_message(@msgctxt + unescape(val[1]) + "\000" + unescape(val[3]), unescape(val[4])) + end + result = "" + result + end +.,., + +module_eval <<'.,.,', 'src/poparser.ry', 76 + def _reduce_10( val, _values, result ) + if val[0].size > 0 + result = val[0] + "\000" + val[1] + else + result = "" + end + result + end +.,., + + # reduce 11 omitted + +module_eval <<'.,.,', 'src/poparser.ry', 84 + def _reduce_12( val, _values, result ) + result = val[2] + result + end +.,., + +module_eval <<'.,.,', 'src/poparser.ry', 91 + def _reduce_13( val, _values, result ) + on_comment(val[0]) + result + end +.,., + +module_eval <<'.,.,', 'src/poparser.ry', 99 + def _reduce_14( val, _values, result ) + result = val.delete_if{|item| item == ""}.join + result + end +.,., + +module_eval <<'.,.,', 'src/poparser.ry', 103 + def _reduce_15( val, _values, result ) + result = val[0] + result + end +.,., + + def _reduce_none( val, _values, result ) + result + end + + end # class PoParser + +end # module GetText diff --git a/lib/mcollective/vendor/i18n/lib/i18n/interpolate/ruby.rb b/lib/mcollective/vendor/i18n/lib/i18n/interpolate/ruby.rb new file mode 100644 index 0000000..29b2814 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/interpolate/ruby.rb @@ -0,0 +1,31 @@ +# heavily based on Masao Mutoh's gettext String interpolation extension +# http://github.com/mutoh/gettext/blob/f6566738b981fe0952548c421042ad1e0cdfb31e/lib/gettext/core_ext/string.rb + +module I18n + INTERPOLATION_PATTERN = Regexp.union( + /%%/, + /%\{(\w+)\}/, # matches placeholders like "%{foo}" + /%<(\w+)>(.*?\d*\.?\d*[bBdiouxXeEfgGcps])/ # matches placeholders like "%.d" + ) + + class << self + def interpolate(string, values) + raise ReservedInterpolationKey.new($1.to_sym, string) if string =~ RESERVED_KEYS_PATTERN + raise ArgumentError.new('Interpolation values must be a Hash.') unless values.kind_of?(Hash) + interpolate_hash(string, values) + end + + def interpolate_hash(string, values) + string.gsub(INTERPOLATION_PATTERN) do |match| + if match == '%%' + '%' + else + key = ($1 || $2).to_sym + value = values.key?(key) ? values[key] : raise(MissingInterpolationArgument.new(values, string)) + value = value.call(values) if value.respond_to?(:call) + $3 ? sprintf("%#{$3}", value) : value + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/locale.rb b/lib/mcollective/vendor/i18n/lib/i18n/locale.rb new file mode 100644 index 0000000..4f9d026 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/locale.rb @@ -0,0 +1,6 @@ +module I18n + module Locale + autoload :Fallbacks, 'i18n/locale/fallbacks' + autoload :Tag, 'i18n/locale/tag' + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/locale/fallbacks.rb b/lib/mcollective/vendor/i18n/lib/i18n/locale/fallbacks.rb new file mode 100644 index 0000000..08bf6f5 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/locale/fallbacks.rb @@ -0,0 +1,96 @@ +# Locale Fallbacks +# +# Extends the I18n module to hold a fallbacks instance which is set to an +# instance of I18n::Locale::Fallbacks by default but can be swapped with a +# different implementation. +# +# Locale fallbacks will compute a number of fallback locales for a given locale. +# For example: +# +#

+# I18n.fallbacks[:"es-MX"] # => [:"es-MX", :es, :en] 
+# +# Locale fallbacks always fall back to +# +# * all parent locales of a given locale (e.g. :es for :"es-MX") first, +# * the current default locales and all of their parents second +# +# The default locales are set to [I18n.default_locale] by default but can be +# set to something else. +# +# One can additionally add any number of additional fallback locales manually. +# These will be added before the default locales to the fallback chain. For +# example: +# +# # using the default locale as default fallback locale +# +# I18n.default_locale = :"en-US" +# I18n.fallbacks = I18n::Locale::Fallbacks.new(:"de-AT" => :"de-DE") +# I18n.fallbacks[:"de-AT"] # => [:"de-AT", :"de-DE", :de, :"en-US", :en] +# +# # using a custom locale as default fallback locale +# +# I18n.fallbacks = I18n::Locale::Fallbacks.new(:"en-GB", :"de-AT" => :de, :"de-CH" => :de) +# I18n.fallbacks[:"de-AT"] # => [:"de-AT", :de, :"en-GB", :en] +# I18n.fallbacks[:"de-CH"] # => [:"de-CH", :de, :"en-GB", :en] +# +# # mapping fallbacks to an existing instance +# +# # people speaking Catalan also speak Spanish as spoken in Spain +# fallbacks = I18n.fallbacks +# fallbacks.map(:ca => :"es-ES") +# fallbacks[:ca] # => [:ca, :"es-ES", :es, :"en-US", :en] +# +# # people speaking Arabian as spoken in Palestine also speak Hebrew as spoken in Israel +# fallbacks.map(:"ar-PS" => :"he-IL") +# fallbacks[:"ar-PS"] # => [:"ar-PS", :ar, :"he-IL", :he, :"en-US", :en] +# fallbacks[:"ar-EG"] # => [:"ar-EG", :ar, :"en-US", :en] +# +# # people speaking Sami as spoken in Finnland also speak Swedish and Finnish as spoken in Finnland +# fallbacks.map(:sms => [:"se-FI", :"fi-FI"]) +# fallbacks[:sms] # => [:sms, :"se-FI", :se, :"fi-FI", :fi, :"en-US", :en] + +module I18n + module Locale + class Fallbacks < Hash + def initialize(*mappings) + @map = {} + map(mappings.pop) if mappings.last.is_a?(Hash) + self.defaults = mappings.empty? ? [I18n.default_locale.to_sym] : mappings + end + + def defaults=(defaults) + @defaults = defaults.map { |default| compute(default, false) }.flatten + end + attr_reader :defaults + + def [](locale) + raise InvalidLocale.new(locale) if locale.nil? + locale = locale.to_sym + super || store(locale, compute(locale)) + end + + def map(mappings) + mappings.each do |from, to| + from, to = from.to_sym, Array(to) + to.each do |_to| + @map[from] ||= [] + @map[from] << _to.to_sym + end + end + end + + protected + + def compute(tags, include_defaults = true, exclude = []) + result = Array(tags).collect do |tag| + tags = I18n::Locale::Tag.tag(tag).self_and_parents.map! { |t| t.to_sym } - exclude + tags.each { |_tag| tags += compute(@map[_tag], false, exclude + tags) if @map[_tag] } + tags + end.flatten + result.push(*defaults) if include_defaults + result.uniq.compact + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/locale/tag.rb b/lib/mcollective/vendor/i18n/lib/i18n/locale/tag.rb new file mode 100644 index 0000000..a640b44 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/locale/tag.rb @@ -0,0 +1,28 @@ +# encoding: utf-8 + +module I18n + module Locale + module Tag + autoload :Parents, 'i18n/locale/tag/parents' + autoload :Rfc4646, 'i18n/locale/tag/rfc4646' + autoload :Simple, 'i18n/locale/tag/simple' + + class << self + # Returns the current locale tag implementation. Defaults to +I18n::Locale::Tag::Simple+. + def implementation + @@implementation ||= Simple + end + + # Sets the current locale tag implementation. Use this to set a different locale tag implementation. + def implementation=(implementation) + @@implementation = implementation + end + + # Factory method for locale tags. Delegates to the current locale tag implementation. + def tag(tag) + implementation.tag(tag) + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/parents.rb b/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/parents.rb new file mode 100644 index 0000000..ec53060 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/parents.rb @@ -0,0 +1,22 @@ +module I18n + module Locale + module Tag + module Parents + def parent + @parent ||= begin + segs = to_a.compact + segs.length > 1 ? self.class.tag(*segs[0..(segs.length-2)].join('-')) : nil + end + end + + def self_and_parents + @self_and_parents ||= [self] + parents + end + + def parents + @parents ||= ([parent] + (parent ? parent.parents : [])).compact + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/rfc4646.rb b/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/rfc4646.rb new file mode 100644 index 0000000..4ce4c75 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/rfc4646.rb @@ -0,0 +1,74 @@ +# RFC 4646/47 compliant Locale tag implementation that parses locale tags to +# subtags such as language, script, region, variant etc. +# +# For more information see by http://en.wikipedia.org/wiki/IETF_language_tag +# +# Rfc4646::Parser does not implement grandfathered tags. + +module I18n + module Locale + module Tag + RFC4646_SUBTAGS = [ :language, :script, :region, :variant, :extension, :privateuse, :grandfathered ] + RFC4646_FORMATS = { :language => :downcase, :script => :capitalize, :region => :upcase, :variant => :downcase } + + class Rfc4646 < Struct.new(*RFC4646_SUBTAGS) + class << self + # Parses the given tag and returns a Tag instance if it is valid. + # Returns false if the given tag is not valid according to RFC 4646. + def tag(tag) + matches = parser.match(tag) + new(*matches) if matches + end + + def parser + @@parser ||= Rfc4646::Parser + end + + def parser=(parser) + @@parser = parser + end + end + + include Parents + + RFC4646_FORMATS.each do |name, format| + define_method(name) { self[name].send(format) unless self[name].nil? } + end + + def to_sym + to_s.to_sym + end + + def to_s + @tag ||= to_a.compact.join("-") + end + + def to_a + members.collect { |attr| self.send(attr) } + end + + module Parser + PATTERN = %r{\A(?: + ([a-z]{2,3}(?:(?:-[a-z]{3}){0,3})?|[a-z]{4}|[a-z]{5,8}) # language + (?:-([a-z]{4}))? # script + (?:-([a-z]{2}|\d{3}))? # region + (?:-([0-9a-z]{5,8}|\d[0-9a-z]{3}))* # variant + (?:-([0-9a-wyz](?:-[0-9a-z]{2,8})+))* # extension + (?:-(x(?:-[0-9a-z]{1,8})+))?| # privateuse subtag + (x(?:-[0-9a-z]{1,8})+)| # privateuse tag + /* ([a-z]{1,3}(?:-[0-9a-z]{2,8}){1,2}) */ # grandfathered + )\z}xi + + class << self + def match(tag) + c = PATTERN.match(tag.to_s).captures + c[0..4] << (c[5].nil? ? c[6] : c[5]) << c[7] # TODO c[7] is grandfathered, throw a NotImplemented exception here? + rescue + false + end + end + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/simple.rb b/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/simple.rb new file mode 100644 index 0000000..68642a1 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/locale/tag/simple.rb @@ -0,0 +1,39 @@ +# Simple Locale tag implementation that computes subtags by simply splitting +# the locale tag at '-' occurences. +module I18n + module Locale + module Tag + class Simple + class << self + def tag(tag) + new(tag) + end + end + + include Parents + + attr_reader :tag + + def initialize(*tag) + @tag = tag.join('-').to_sym + end + + def subtags + @subtags = tag.to_s.split('-').map { |subtag| subtag.to_s } + end + + def to_sym + tag + end + + def to_s + tag.to_s + end + + def to_a + subtags + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests.rb new file mode 100644 index 0000000..554cdef --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests.rb @@ -0,0 +1,12 @@ +module I18n + module Tests + autoload :Basics, 'i18n/tests/basics' + autoload :Defaults, 'i18n/tests/defaults' + autoload :Interpolation, 'i18n/tests/interpolation' + autoload :Link, 'i18n/tests/link' + autoload :Localization, 'i18n/tests/localization' + autoload :Lookup, 'i18n/tests/lookup' + autoload :Pluralization, 'i18n/tests/pluralization' + autoload :Procs, 'i18n/tests/procs' + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/basics.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/basics.rb new file mode 100644 index 0000000..101e7c5 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/basics.rb @@ -0,0 +1,54 @@ +module I18n + module Tests + module Basics + def teardown + I18n.available_locales = nil + end + + test "available_locales returns the locales stored to the backend by default" do + I18n.backend.store_translations('de', :foo => 'bar') + I18n.backend.store_translations('en', :foo => 'foo') + + assert I18n.available_locales.include?(:de) + assert I18n.available_locales.include?(:en) + end + + test "available_locales can be set to something else independently from the actual locale data" do + I18n.backend.store_translations('de', :foo => 'bar') + I18n.backend.store_translations('en', :foo => 'foo') + + I18n.available_locales = :foo + assert_equal [:foo], I18n.available_locales + + I18n.available_locales = [:foo, 'bar'] + assert_equal [:foo, :bar], I18n.available_locales + + I18n.available_locales = nil + assert I18n.available_locales.include?(:de) + assert I18n.available_locales.include?(:en) + end + + test "available_locales memoizes when set explicitely" do + I18n.backend.expects(:available_locales).never + I18n.available_locales = [:foo] + I18n.backend.store_translations('de', :bar => 'baz') + I18n.reload! + assert_equal [:foo], I18n.available_locales + end + + test "available_locales delegates to the backend when not set explicitely" do + I18n.backend.expects(:available_locales).twice + assert_equal I18n.available_locales, I18n.available_locales + end + + test "storing a nil value as a translation removes it from the available locale data" do + I18n.backend.store_translations(:en, :to_be_deleted => 'bar') + assert_equal 'bar', I18n.t(:to_be_deleted, :default => 'baz') + + I18n.cache_store.clear if I18n.respond_to?(:cache_store) && I18n.cache_store + I18n.backend.store_translations(:en, :to_be_deleted => nil) + assert_equal 'baz', I18n.t(:to_be_deleted, :default => 'baz') + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/defaults.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/defaults.rb new file mode 100644 index 0000000..081dcbd --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/defaults.rb @@ -0,0 +1,40 @@ +# encoding: utf-8 + +module I18n + module Tests + module Defaults + def setup + super + I18n.backend.store_translations(:en, :foo => { :bar => 'bar', :baz => 'baz' }) + end + + test "defaults: given nil as a key it returns the given default" do + assert_equal 'default', I18n.t(nil, :default => 'default') + end + + test "defaults: given a symbol as a default it translates the symbol" do + assert_equal 'bar', I18n.t(nil, :default => :'foo.bar') + end + + test "defaults: given a symbol as a default and a scope it stays inside the scope when looking up the symbol" do + assert_equal 'bar', I18n.t(:missing, :default => :bar, :scope => :foo) + end + + test "defaults: given an array as a default it returns the first match" do + assert_equal 'bar', I18n.t(:does_not_exist, :default => [:does_not_exist_2, :'foo.bar']) + end + + test "defaults: given an array of missing keys it raises a MissingTranslationData exception" do + assert_raise I18n::MissingTranslationData do + I18n.t(:does_not_exist, :default => [:does_not_exist_2, :does_not_exist_3], :raise => true) + end + end + + test "defaults: using a custom scope separator" do + # data must have been stored using the custom separator when using the ActiveRecord backend + I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' }) + assert_equal 'bar', I18n.t(nil, :default => :'foo|bar', :separator => '|') + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/interpolation.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/interpolation.rb new file mode 100644 index 0000000..06613f2 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/interpolation.rb @@ -0,0 +1,133 @@ +# encoding: utf-8 + +module I18n + module Tests + module Interpolation + # If no interpolation parameter is not given, I18n should not alter the string. + # This behavior is due to three reasons: + # + # * Checking interpolation keys in all strings hits performance, badly; + # + # * This allows us to retrieve untouched values through I18n. For example + # I could have a middleware that returns I18n lookup results in JSON + # to be processed through Javascript. Leaving the keys untouched allows + # the interpolation to happen at the javascript level; + # + # * Security concerns: if I allow users to translate a web site, they can + # insert %{} in messages causing the I18n lookup to fail in every request. + # + test "interpolation: given no values it does not alter the string" do + assert_equal 'Hi %{name}!', interpolate(:default => 'Hi %{name}!') + end + + test "interpolation: given values it interpolates them into the string" do + assert_equal 'Hi David!', interpolate(:default => 'Hi %{name}!', :name => 'David') + end + + test "interpolation: given a nil value it still interpolates it into the string" do + assert_equal 'Hi !', interpolate(:default => 'Hi %{name}!', :name => nil) + end + + test "interpolation: given a lambda as a value it calls it if the string contains the key" do + assert_equal 'Hi David!', interpolate(:default => 'Hi %{name}!', :name => lambda { |*args| 'David' }) + end + + test "interpolation: given a lambda as a value it does not call it if the string does not contain the key" do + assert_nothing_raised { interpolate(:default => 'Hi!', :name => lambda { |*args| raise 'fail' }) } + end + + test "interpolation: given values but missing a key it raises I18n::MissingInterpolationArgument" do + assert_raise(I18n::MissingInterpolationArgument) do + interpolate(:default => '%{foo}', :bar => 'bar') + end + end + + test "interpolation: it does not raise I18n::MissingInterpolationArgument for escaped variables" do + assert_nothing_raised(I18n::MissingInterpolationArgument) do + assert_equal 'Barr %{foo}', interpolate(:default => '%{bar} %%{foo}', :bar => 'Barr') + end + end + + test "interpolation: it does not change the original, stored translation string" do + I18n.backend.store_translations(:en, :interpolate => 'Hi %{name}!') + assert_equal 'Hi David!', interpolate(:interpolate, :name => 'David') + assert_equal 'Hi Yehuda!', interpolate(:interpolate, :name => 'Yehuda') + end + + test "interpolation: given the translation is in utf-8 it still works" do + assert_equal 'Häi David!', interpolate(:default => 'Häi %{name}!', :name => 'David') + end + + test "interpolation: given the value is in utf-8 it still works" do + assert_equal 'Hi ゆきひろ!', interpolate(:default => 'Hi %{name}!', :name => 'ゆきひろ') + end + + test "interpolation: given the translation and the value are in utf-8 it still works" do + assert_equal 'こんにちは、ゆきひろさん!', interpolate(:default => 'こんにちは、%{name}さん!', :name => 'ゆきひろ') + end + + if Kernel.const_defined?(:Encoding) + test "interpolation: given a euc-jp translation and a utf-8 value it raises Encoding::CompatibilityError" do + assert_raise(Encoding::CompatibilityError) do + interpolate(:default => euc_jp('こんにちは、%{name}さん!'), :name => 'ゆきひろ') + end + end + + test "interpolation: given a utf-8 translation and a euc-jp value it raises Encoding::CompatibilityError" do + assert_raise(Encoding::CompatibilityError) do + interpolate(:default => 'こんにちは、%{name}さん!', :name => euc_jp('ゆきひろ')) + end + end + + test "interpolation: ASCII strings in the backend should be encoded to UTF8 if interpolation options are in UTF8" do + I18n.backend.store_translations 'en', 'encoding' => ('%{who} let me go'.force_encoding("ASCII")) + result = I18n.t 'encoding', :who => "måmmå miå" + assert_equal Encoding::UTF_8, result.encoding + end + + test "interpolation: UTF8 strings in the backend are still returned as UTF8 with ASCII interpolation" do + I18n.backend.store_translations 'en', 'encoding' => 'måmmå miå %{what}' + result = I18n.t 'encoding', :what => 'let me go'.force_encoding("ASCII") + assert_equal Encoding::UTF_8, result.encoding + end + + test "interpolation: UTF8 strings in the backend are still returned as UTF8 even with numbers interpolation" do + I18n.backend.store_translations 'en', 'encoding' => '%{count} times: måmmå miå' + result = I18n.t 'encoding', :count => 3 + assert_equal Encoding::UTF_8, result.encoding + end + end + + test "interpolation: given a translations containing a reserved key it raises I18n::ReservedInterpolationKey" do + assert_raise(I18n::ReservedInterpolationKey) { interpolate(:default => '%{default}', :foo => :bar) } + assert_raise(I18n::ReservedInterpolationKey) { interpolate(:default => '%{scope}', :foo => :bar) } + assert_raise(I18n::ReservedInterpolationKey) { interpolate(:default => '%{separator}', :foo => :bar) } + end + + protected + + def capture(stream) + begin + stream = stream.to_s + eval "$#{stream} = StringIO.new" + yield + result = eval("$#{stream}").string + ensure + eval("$#{stream} = #{stream.upcase}") + end + + result + end + + def euc_jp(string) + string.encode!(Encoding::EUC_JP) + end + + def interpolate(*args) + options = args.last.is_a?(Hash) ? args.pop : {} + key = args.pop + I18n.backend.translate('en', key, options) + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/link.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/link.rb new file mode 100644 index 0000000..da84a2c --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/link.rb @@ -0,0 +1,56 @@ +# encoding: utf-8 + +module I18n + module Tests + module Link + test "linked lookup: if a key resolves to a symbol it looks up the symbol" do + I18n.backend.store_translations 'en', { + :link => :linked, + :linked => 'linked' + } + assert_equal 'linked', I18n.backend.translate('en', :link) + end + + test "linked lookup: if a key resolves to a dot-separated symbol it looks up the symbol" do + I18n.backend.store_translations 'en', { + :link => :"foo.linked", + :foo => { :linked => 'linked' } + } + assert_equal('linked', I18n.backend.translate('en', :link)) + end + + test "linked lookup: if a dot-separated key resolves to a symbol it looks up the symbol" do + I18n.backend.store_translations 'en', { + :foo => { :link => :linked }, + :linked => 'linked' + } + assert_equal('linked', I18n.backend.translate('en', :'foo.link')) + end + + test "linked lookup: if a dot-separated key resolves to a dot-separated symbol it looks up the symbol" do + I18n.backend.store_translations 'en', { + :foo => { :link => :"bar.linked" }, + :bar => { :linked => 'linked' } + } + assert_equal('linked', I18n.backend.translate('en', :'foo.link')) + end + + test "linked lookup: links always refer to the absolute key" do + I18n.backend.store_translations 'en', { + :foo => { :link => :linked, :linked => 'linked in foo' }, + :linked => 'linked absolutely' + } + assert_equal 'linked absolutely', I18n.backend.translate('en', :link, :scope => :foo) + end + + test "linked lookup: a link can resolve to a namespace in the middle of a dot-separated key" do + I18n.backend.store_translations 'en', { + :activemodel => { :errors => { :messages => { :blank => "can't be blank" } } }, + :activerecord => { :errors => { :messages => :"activemodel.errors.messages" } } + } + assert_equal "can't be blank", I18n.t(:"activerecord.errors.messages.blank") + assert_equal "can't be blank", I18n.t(:"activerecord.errors.messages.blank") + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/localization.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization.rb new file mode 100644 index 0000000..53b1502 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization.rb @@ -0,0 +1,19 @@ +module I18n + module Tests + module Localization + autoload :Date, 'i18n/tests/localization/date' + autoload :DateTime, 'i18n/tests/localization/date_time' + autoload :Time, 'i18n/tests/localization/time' + autoload :Procs, 'i18n/tests/localization/procs' + + def self.included(base) + base.class_eval do + include I18n::Tests::Localization::Date + include I18n::Tests::Localization::DateTime + include I18n::Tests::Localization::Procs + include I18n::Tests::Localization::Time + end + end + end + end +end \ No newline at end of file diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/date.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/date.rb new file mode 100644 index 0000000..a866f90 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/date.rb @@ -0,0 +1,84 @@ +# encoding: utf-8 + +module I18n + module Tests + module Localization + module Date + def setup + super + setup_date_translations + @date = ::Date.new(2008, 3, 1) + end + + test "localize Date: given the short format it uses it" do + # TODO should be Mrz, shouldn't it? + assert_equal '01. Mar', I18n.l(@date, :format => :short, :locale => :de) + end + + test "localize Date: given the long format it uses it" do + assert_equal '01. März 2008', I18n.l(@date, :format => :long, :locale => :de) + end + + test "localize Date: given the default format it uses it" do + assert_equal '01.03.2008', I18n.l(@date, :format => :default, :locale => :de) + end + + test "localize Date: given a day name format it returns the correct day name" do + assert_equal 'Samstag', I18n.l(@date, :format => '%A', :locale => :de) + end + + test "localize Date: given an abbreviated day name format it returns the correct abbreviated day name" do + assert_equal 'Sa', I18n.l(@date, :format => '%a', :locale => :de) + end + + test "localize Date: given a month name format it returns the correct month name" do + assert_equal 'März', I18n.l(@date, :format => '%B', :locale => :de) + end + + test "localize Date: given an abbreviated month name format it returns the correct abbreviated month name" do + # TODO should be Mrz, shouldn't it? + assert_equal 'Mar', I18n.l(@date, :format => '%b', :locale => :de) + end + + test "localize Date: given an unknown format it does not fail" do + assert_nothing_raised { I18n.l(@date, :format => '%x') } + end + + test "localize Date: given nil it raises I18n::ArgumentError" do + assert_raise(I18n::ArgumentError) { I18n.l(nil) } + end + + test "localize Date: given a plain Object it raises I18n::ArgumentError" do + assert_raise(I18n::ArgumentError) { I18n.l(Object.new) } + end + + test "localize Date: given a format is missing it raises I18n::MissingTranslationData" do + assert_raise(I18n::MissingTranslationData) { I18n.l(@date, :format => :missing) } + end + + test "localize Date: it does not alter the format string" do + assert_equal '01. Februar 2009', I18n.l(::Date.parse('2009-02-01'), :format => :long, :locale => :de) + assert_equal '01. Oktober 2009', I18n.l(::Date.parse('2009-10-01'), :format => :long, :locale => :de) + end + + protected + + def setup_date_translations + I18n.backend.store_translations :de, { + :date => { + :formats => { + :default => "%d.%m.%Y", + :short => "%d. %b", + :long => "%d. %B %Y", + }, + :day_names => %w(Sonntag Montag Dienstag Mittwoch Donnerstag Freitag Samstag), + :abbr_day_names => %w(So Mo Di Mi Do Fr Sa), + :month_names => %w(Januar Februar März April Mai Juni Juli August September Oktober November Dezember).unshift(nil), + :abbr_month_names => %w(Jan Feb Mar Apr Mai Jun Jul Aug Sep Okt Nov Dez).unshift(nil) + } + } + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/date_time.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/date_time.rb new file mode 100644 index 0000000..a926d1c --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/date_time.rb @@ -0,0 +1,77 @@ +# encoding: utf-8 + +module I18n + module Tests + module Localization + module DateTime + def setup + super + setup_datetime_translations + @datetime = ::DateTime.new(2008, 3, 1, 6) + @other_datetime = ::DateTime.new(2008, 3, 1, 18) + end + + test "localize DateTime: given the short format it uses it" do + # TODO should be Mrz, shouldn't it? + assert_equal '01. Mar 06:00', I18n.l(@datetime, :format => :short, :locale => :de) + end + + test "localize DateTime: given the long format it uses it" do + assert_equal '01. März 2008 06:00', I18n.l(@datetime, :format => :long, :locale => :de) + end + + test "localize DateTime: given the default format it uses it" do + # TODO should be Mrz, shouldn't it? + assert_equal 'Sa, 01. Mar 2008 06:00:00 +0000', I18n.l(@datetime, :format => :default, :locale => :de) + end + + test "localize DateTime: given a day name format it returns the correct day name" do + assert_equal 'Samstag', I18n.l(@datetime, :format => '%A', :locale => :de) + end + + test "localize DateTime: given an abbreviated day name format it returns the correct abbreviated day name" do + assert_equal 'Sa', I18n.l(@datetime, :format => '%a', :locale => :de) + end + + test "localize DateTime: given a month name format it returns the correct month name" do + assert_equal 'März', I18n.l(@datetime, :format => '%B', :locale => :de) + end + + test "localize DateTime: given an abbreviated month name format it returns the correct abbreviated month name" do + # TODO should be Mrz, shouldn't it? + assert_equal 'Mar', I18n.l(@datetime, :format => '%b', :locale => :de) + end + + test "localize DateTime: given a meridian indicator format it returns the correct meridian indicator" do + assert_equal 'am', I18n.l(@datetime, :format => '%p', :locale => :de) + assert_equal 'pm', I18n.l(@other_datetime, :format => '%p', :locale => :de) + end + + test "localize DateTime: given an unknown format it does not fail" do + assert_nothing_raised { I18n.l(@datetime, :format => '%x') } + end + + test "localize DateTime: given a format is missing it raises I18n::MissingTranslationData" do + assert_raise(I18n::MissingTranslationData) { I18n.l(@datetime, :format => :missing) } + end + + protected + + def setup_datetime_translations + # time translations might have been set up in Tests::Api::Localization::Time + I18n.backend.store_translations :de, { + :time => { + :formats => { + :default => "%a, %d. %b %Y %H:%M:%S %z", + :short => "%d. %b %H:%M", + :long => "%d. %B %Y %H:%M" + }, + :am => 'am', + :pm => 'pm' + } + } + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/procs.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/procs.rb new file mode 100644 index 0000000..83d24bc --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/procs.rb @@ -0,0 +1,116 @@ +# encoding: utf-8 + +module I18n + module Tests + module Localization + module Procs + test "localize: using day names from lambdas" do + setup_time_proc_translations + time = ::Time.utc(2008, 3, 1, 6, 0) + assert_match(/Суббота/, I18n.l(time, :format => "%A, %d %B", :locale => :ru)) + assert_match(/суббота/, I18n.l(time, :format => "%d %B (%A)", :locale => :ru)) + end + + test "localize: using month names from lambdas" do + setup_time_proc_translations + time = ::Time.utc(2008, 3, 1, 6, 0) + assert_match(/марта/, I18n.l(time, :format => "%d %B %Y", :locale => :ru)) + assert_match(/Март /, I18n.l(time, :format => "%B %Y", :locale => :ru)) + end + + test "localize: using abbreviated day names from lambdas" do + setup_time_proc_translations + time = ::Time.utc(2008, 3, 1, 6, 0) + assert_match(/марта/, I18n.l(time, :format => "%d %b %Y", :locale => :ru)) + assert_match(/март /, I18n.l(time, :format => "%b %Y", :locale => :ru)) + end + + test "localize Date: given a format that resolves to a Proc it calls the Proc with the object" do + setup_time_proc_translations + date = ::Date.new(2008, 3, 1, 6) + assert_equal '[Sat, 01 Mar 2008, {}]', I18n.l(date, :format => :proc, :locale => :ru) + end + + test "localize Date: given a format that resolves to a Proc it calls the Proc with the object and extra options" do + setup_time_proc_translations + date = ::Date.new(2008, 3, 1, 6) + assert_equal '[Sat, 01 Mar 2008, {:foo=>"foo"}]', I18n.l(date, :format => :proc, :foo => 'foo', :locale => :ru) + end + + test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object" do + setup_time_proc_translations + datetime = ::DateTime.new(2008, 3, 1, 6) + assert_equal '[Sat, 01 Mar 2008 06:00:00 +00:00, {}]', I18n.l(datetime, :format => :proc, :locale => :ru) + end + + test "localize DateTime: given a format that resolves to a Proc it calls the Proc with the object and extra options" do + setup_time_proc_translations + datetime = ::DateTime.new(2008, 3, 1, 6) + assert_equal '[Sat, 01 Mar 2008 06:00:00 +00:00, {:foo=>"foo"}]', I18n.l(datetime, :format => :proc, :foo => 'foo', :locale => :ru) + end + + test "localize Time: given a format that resolves to a Proc it calls the Proc with the object" do + setup_time_proc_translations + time = ::Time.utc(2008, 3, 1, 6, 0) + assert_equal inspect_args([time, {}]), I18n.l(time, :format => :proc, :locale => :ru) + end + + test "localize Time: given a format that resolves to a Proc it calls the Proc with the object and extra options" do + setup_time_proc_translations + time = ::Time.utc(2008, 3, 1, 6, 0) + options = { :foo => 'foo' } + assert_equal inspect_args([time, options]), I18n.l(time, options.merge(:format => :proc, :locale => :ru)) + end + + protected + + def inspect_args(args) + args = args.map do |arg| + case arg + when ::Time, ::DateTime + arg.strftime('%a, %d %b %Y %H:%M:%S %Z').sub('+0000', '+00:00') + when ::Date + arg.strftime('%a, %d %b %Y') + when Hash + arg.delete(:fallback) + arg.inspect + else + arg.inspect + end + end + "[#{args.join(', ')}]" + end + + def setup_time_proc_translations + I18n.backend.store_translations :ru, { + :time => { + :formats => { + :proc => lambda { |*args| inspect_args(args) } + } + }, + :date => { + :formats => { + :proc => lambda { |*args| inspect_args(args) } + }, + :'day_names' => lambda { |key, options| + (options[:format] =~ /^%A/) ? + %w(Воскресенье Понедельник Вторник Среда Четверг Пятница Суббота) : + %w(воскресенье понедельник вторник среда четверг пятница суббота) + }, + :'month_names' => lambda { |key, options| + (options[:format] =~ /(%d|%e)(\s*)?(%B)/) ? + %w(января февраля марта апреля мая июня июля августа сентября октября ноября декабря).unshift(nil) : + %w(Январь Февраль Март Апрель Май Июнь Июль Август Сентябрь Октябрь Ноябрь Декабрь).unshift(nil) + }, + :'abbr_month_names' => lambda { |key, options| + (options[:format] =~ /(%d|%e)(\s*)(%b)/) ? + %w(янв. февр. марта апр. мая июня июля авг. сент. окт. нояб. дек.).unshift(nil) : + %w(янв. февр. март апр. май июнь июль авг. сент. окт. нояб. дек.).unshift(nil) + }, + } + } + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/time.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/time.rb new file mode 100644 index 0000000..599f21f --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/localization/time.rb @@ -0,0 +1,76 @@ +# encoding: utf-8 + +module I18n + module Tests + module Localization + module Time + def setup + super + setup_time_translations + @time = ::Time.utc(2008, 3, 1, 6, 0) + @other_time = ::Time.utc(2008, 3, 1, 18, 0) + end + + test "localize Time: given the short format it uses it" do + # TODO should be Mrz, shouldn't it? + assert_equal '01. Mar 06:00', I18n.l(@time, :format => :short, :locale => :de) + end + + test "localize Time: given the long format it uses it" do + assert_equal '01. März 2008 06:00', I18n.l(@time, :format => :long, :locale => :de) + end + + # TODO Seems to break on Windows because ENV['TZ'] is ignored. What's a better way to do this? + # def test_localize_given_the_default_format_it_uses_it + # assert_equal 'Sa, 01. Mar 2008 06:00:00 +0000', I18n.l(@time, :format => :default, :locale => :de) + # end + + test "localize Time: given a day name format it returns the correct day name" do + assert_equal 'Samstag', I18n.l(@time, :format => '%A', :locale => :de) + end + + test "localize Time: given an abbreviated day name format it returns the correct abbreviated day name" do + assert_equal 'Sa', I18n.l(@time, :format => '%a', :locale => :de) + end + + test "localize Time: given a month name format it returns the correct month name" do + assert_equal 'März', I18n.l(@time, :format => '%B', :locale => :de) + end + + test "localize Time: given an abbreviated month name format it returns the correct abbreviated month name" do + # TODO should be Mrz, shouldn't it? + assert_equal 'Mar', I18n.l(@time, :format => '%b', :locale => :de) + end + + test "localize Time: given a meridian indicator format it returns the correct meridian indicator" do + assert_equal 'am', I18n.l(@time, :format => '%p', :locale => :de) + assert_equal 'pm', I18n.l(@other_time, :format => '%p', :locale => :de) + end + + test "localize Time: given an unknown format it does not fail" do + assert_nothing_raised { I18n.l(@time, :format => '%x') } + end + + test "localize Time: given a format is missing it raises I18n::MissingTranslationData" do + assert_raise(I18n::MissingTranslationData) { I18n.l(@time, :format => :missing) } + end + + protected + + def setup_time_translations + I18n.backend.store_translations :de, { + :time => { + :formats => { + :default => "%a, %d. %b %Y %H:%M:%S %z", + :short => "%d. %b %H:%M", + :long => "%d. %B %Y %H:%M", + }, + :am => 'am', + :pm => 'pm' + } + } + end + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/lookup.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/lookup.rb new file mode 100644 index 0000000..63c21ac --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/lookup.rb @@ -0,0 +1,74 @@ +# encoding: utf-8 + +module I18n + module Tests + module Lookup + def setup + super + I18n.backend.store_translations(:en, :foo => { :bar => 'bar', :baz => 'baz' }, :falsy => false, :truthy => true, + :string => "a", :array => %w(a b c), :hash => { "a" => "b" }) + end + + test "lookup: it returns a string" do + assert_equal("a", I18n.t(:string)) + end + + test "lookup: it returns hash" do + assert_equal({ :a => "b" }, I18n.t(:hash)) + end + + test "lookup: it returns a array" do + assert_equal(%w(a b c), I18n.t(:array)) + end + + test "lookup: it returns a native true" do + assert I18n.t(:truthy) === true + end + + test "lookup: it returns a native false" do + assert I18n.t(:falsy) === false + end + + test "lookup: given a missing key, no default and no raise option it returns an error message" do + assert_equal "translation missing: en.missing", I18n.t(:missing) + end + + test "lookup: given a missing key, no default and the raise option it raises MissingTranslationData" do + assert_raise(I18n::MissingTranslationData) { I18n.t(:missing, :raise => true) } + end + + test "lookup: does not raise an exception if no translation data is present for the given locale" do + assert_nothing_raised { I18n.t(:foo, :locale => :xx) } + end + + test "lookup: given an array of keys it translates all of them" do + assert_equal %w(bar baz), I18n.t([:bar, :baz], :scope => [:foo]) + end + + test "lookup: using a custom scope separator" do + # data must have been stored using the custom separator when using the ActiveRecord backend + I18n.backend.store_translations(:en, { :foo => { :bar => 'bar' } }, { :separator => '|' }) + assert_equal 'bar', I18n.t('foo|bar', :separator => '|') + end + + # In fact it probably *should* fail but Rails currently relies on using the default locale instead. + # So we'll stick to this for now until we get it fixed in Rails. + test "lookup: given nil as a locale it does not raise but use the default locale" do + # assert_raise(I18n::InvalidLocale) { I18n.t(:bar, :locale => nil) } + assert_nothing_raised { I18n.t(:bar, :locale => nil) } + end + + test "lookup: a resulting String is not frozen" do + assert !I18n.t(:string).frozen? + end + + test "lookup: a resulting Array is not frozen" do + assert !I18n.t(:array).frozen? + end + + test "lookup: a resulting Hash is not frozen" do + assert !I18n.t(:hash).frozen? + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/pluralization.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/pluralization.rb new file mode 100644 index 0000000..d3319dc --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/pluralization.rb @@ -0,0 +1,35 @@ +# encoding: utf-8 + +module I18n + module Tests + module Pluralization + test "pluralization: given 0 it returns the :zero translation if it is defined" do + assert_equal 'zero', I18n.t(:default => { :zero => 'zero' }, :count => 0) + end + + test "pluralization: given 0 it returns the :other translation if :zero is not defined" do + assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 0) + end + + test "pluralization: given 1 it returns the singular translation" do + assert_equal 'bar', I18n.t(:default => { :one => 'bar' }, :count => 1) + end + + test "pluralization: given 2 it returns the :other translation" do + assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 2) + end + + test "pluralization: given 3 it returns the :other translation" do + assert_equal 'bars', I18n.t(:default => { :other => 'bars' }, :count => 3) + end + + test "pluralization: given nil it returns the whole entry" do + assert_equal({ :one => 'bar' }, I18n.t(:default => { :one => 'bar' }, :count => nil)) + end + + test "pluralization: given incomplete pluralization data it raises I18n::InvalidPluralizationData" do + assert_raise(I18n::InvalidPluralizationData) { I18n.t(:default => { :one => 'bar' }, :count => 2) } + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/tests/procs.rb b/lib/mcollective/vendor/i18n/lib/i18n/tests/procs.rb new file mode 100644 index 0000000..55ff952 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/tests/procs.rb @@ -0,0 +1,55 @@ +# encoding: utf-8 + +module I18n + module Tests + module Procs + test "lookup: given a translation is a proc it calls the proc with the key and interpolation values" do + I18n.backend.store_translations(:en, :a_lambda => lambda { |*args| filter_args(*args) }) + assert_equal '[:a_lambda, {:foo=>"foo"}]', I18n.t(:a_lambda, :foo => 'foo') + end + + test "defaults: given a default is a Proc it calls it with the key and interpolation values" do + proc = lambda { |*args| filter_args(*args) } + assert_equal '[nil, {:foo=>"foo"}]', I18n.t(nil, :default => proc, :foo => 'foo') + end + + test "defaults: given a default is a key that resolves to a Proc it calls it with the key and interpolation values" do + I18n.backend.store_translations(:en, :a_lambda => lambda { |*args| filter_args(*args) }) + assert_equal '[:a_lambda, {:foo=>"foo"}]', I18n.t(nil, :default => :a_lambda, :foo => 'foo') + assert_equal '[:a_lambda, {:foo=>"foo"}]', I18n.t(nil, :default => [nil, :a_lambda], :foo => 'foo') + end + + test "interpolation: given an interpolation value is a lambda it calls it with key and values before interpolating it" do + proc = lambda { |*args| filter_args(*args) } + assert_match %r(\[\{:foo=>#\}\]), I18n.t(nil, :default => '%{foo}', :foo => proc) + end + + test "interpolation: given a key resolves to a Proc that returns a string then interpolation still works" do + proc = lambda { |*args| "%{foo}: " + filter_args(*args) } + assert_equal 'foo: [nil, {:foo=>"foo"}]', I18n.t(nil, :default => proc, :foo => 'foo') + end + + test "pluralization: given a key resolves to a Proc that returns valid data then pluralization still works" do + proc = lambda { |*args| { :zero => 'zero', :one => 'one', :other => 'other' } } + assert_equal 'zero', I18n.t(:default => proc, :count => 0) + assert_equal 'one', I18n.t(:default => proc, :count => 1) + assert_equal 'other', I18n.t(:default => proc, :count => 2) + end + + test "lookup: given the option :resolve => false was passed it does not resolve proc translations" do + I18n.backend.store_translations(:en, :a_lambda => lambda { |*args| filter_args(*args) }) + assert_equal Proc, I18n.t(:a_lambda, :resolve => false).class + end + + test "lookup: given the option :resolve => false was passed it does not resolve proc default" do + assert_equal Proc, I18n.t(nil, :default => lambda { |*args| filter_args(*args) }, :resolve => false).class + end + + protected + + def filter_args(*args) + args.map {|arg| arg.delete(:fallback) if arg.is_a?(Hash) ; arg }.inspect + end + end + end +end diff --git a/lib/mcollective/vendor/i18n/lib/i18n/version.rb b/lib/mcollective/vendor/i18n/lib/i18n/version.rb new file mode 100644 index 0000000..9249aa8 --- /dev/null +++ b/lib/mcollective/vendor/i18n/lib/i18n/version.rb @@ -0,0 +1,3 @@ +module I18n + VERSION = "0.6.1" +end diff --git a/lib/mcollective/vendor/json/.gitignore b/lib/mcollective/vendor/json/.gitignore new file mode 100644 index 0000000..726ffce --- /dev/null +++ b/lib/mcollective/vendor/json/.gitignore @@ -0,0 +1,8 @@ +.*.sw[pon] +coverage +pkg +.nfs.* +.idea +java/Json.iml +Gemfile.lock +.rvmrc diff --git a/lib/mcollective/vendor/json/.travis.yml b/lib/mcollective/vendor/json/.travis.yml new file mode 100644 index 0000000..cb74e2d --- /dev/null +++ b/lib/mcollective/vendor/json/.travis.yml @@ -0,0 +1,15 @@ +# Passes arguments to bundle install (http://gembundler.com/man/bundle-install.1.html) +bundler_args: --binstubs + +# Specify which ruby versions you wish to run your tests on, each version will be used +rvm: + - 1.8.7 + - 1.9.2 + - 1.9.3 + - rbx + - rbx-2.0 + - ree + - jruby + - ruby-head + +script: "bundle exec rake" diff --git a/lib/mcollective/vendor/json/CHANGES b/lib/mcollective/vendor/json/CHANGES new file mode 100644 index 0000000..42328b7 --- /dev/null +++ b/lib/mcollective/vendor/json/CHANGES @@ -0,0 +1,212 @@ +2013-02-04 (1.5.5) + * Security fix for JSON create_additions default value. It should not be + possible to create additions unless + explicitely requested by setting the create_additions argument to true or + using the JSON.load/dump interface. + * Backport change that corrects Time serialisation/deserialisation on some + platforms. +2011-08-31 (1.5.4) + * Fix memory leak when used from multiple JRuby. (Patch by + jfirebaugh@github). + * Apply patch by Eric Wong that fixes garbage collection problem + reported in https://github.com/flori/json/issues/46. + * Add :quirks_mode option to parser and generator. + * Add support for Rational and Complex number additions via json/add/complex + and json/add/rational requires. +2011-06-20 (1.5.3) + * Alias State#configure method as State#merge to increase duck type synonymy with Hash. + * Add as_json methods in json/add/core, so rails can create its json objects + the new way. +2011-05-11 (1.5.2) + * Apply documentation patch by Cory Monty . + * Add gemspecs for json and json_pure. + * Fix bug in jruby pretty printing. + * Fix bug in object_class and array_class when inheriting from Hash or Array. +2011-01-24 (1.5.1) + * Made rake-compiler build a fat binary gem. This should fix issue + https://github.com/flori/json/issues#issue/54. +2011-01-22 (1.5.0) + * Included Java source codes for the Jruby extension made by Daniel Luz + . + * Output full exception message of deep_const_get to aid debugging. + * Fixed an issue with ruby 1.9 Module#const_defined? method, that was + reported by Riley Goodside. +2010-08-09 (1.4.6) + * Fixed oversight reported in http://github.com/flori/json/issues/closed#issue/23, + always create a new object from the state prototype. + * Made pure and ext api more similar again. +2010-08-07 (1.4.5) + * Manage data structure nesting depth in state object during generation. This + should reduce problems with to_json method definіtions that only have one + argument. + * Some fixes in the state objects and additional tests. +2010-08-06 (1.4.4) + * Fixes build problem for rubinius under OS X, http://github.com/flori/json/issues/closed#issue/25 + * Fixes crashes described in http://github.com/flori/json/issues/closed#issue/21 and + http://github.com/flori/json/issues/closed#issue/23 +2010-05-05 (1.4.3) + * Fixed some test assertions, from Ruby r27587 and r27590, patch by nobu. + * Fixed issue http://github.com/flori/json/issues/#issue/20 reported by + electronicwhisper@github. Thx! +2010-04-26 (1.4.2) + * Applied patch from naruse Yui NARUSE to make building with + Microsoft Visual C possible again. + * Applied patch from devrandom in order to allow building of + json_pure if extensiontask is not present. + * Thanks to Dustin Schneider , who reported a memory + leak, which is fixed in this release. + * Applied 993f261ccb8f911d2ae57e9db48ec7acd0187283 patch from josh@github. +2010-04-25 (1.4.1) + * Fix for a bug reported by Dan DeLeo , caused by T_FIXNUM + being different on 32bit/64bit architectures. +2010-04-23 (1.4.0) + * Major speed improvements and building with simplified + directory/file-structure. + * Extension should at least be comapatible with MRI, YARV and Rubinius. +2010-04-07 (1.2.4) + * Triger const_missing callback to make Rails' dynamic class loading work. +2010-03-11 (1.2.3) + * Added a State#[] method which returns an attribute's value in order to + increase duck type compatibility to Hash. +2010-02-27 (1.2.2) + * Made some changes to make the building of the parser/generator compatible + to Rubinius. +2009-11-25 (1.2.1) + * Added :symbolize_names option to Parser, which returns symbols instead of + strings in object names/keys. +2009-10-01 (1.2.0) + * fast_generate now raises an exeception for nan and infinite floats. + * On Ruby 1.8 json supports parsing of UTF-8, UTF-16BE, UTF-16LE, UTF-32BE, + and UTF-32LE JSON documents now. Under Ruby 1.9 the M17n conversion + functions are used to convert from all supported encodings. ASCII-8BIT + encoded strings are handled like all strings under Ruby 1.8 were. + * Better documentation +2009-08-23 (1.1.9) + * Added forgotten main doc file extra_rdoc_files. +2009-08-23 (1.1.8) + * Applied a patch by OZAWA Sakuro to make json/pure + work in environments that don't provide iconv. + * Applied patch by okkez_ in order to fix Ruby Bug #1768: + http://redmine.ruby-lang.org/issues/show/1768. + * Finally got around to avoid the rather paranoid escaping of ?/ characters + in the generator's output. The parsers aren't affected by this change. + Thanks to Rich Apodaca for the suggestion. +2009-06-29 (1.1.7) + * Security Fix for JSON::Pure::Parser. A specially designed string could + cause catastrophic backtracking in one of the parser's regular expressions + in earlier 1.1.x versions. JSON::Ext::Parser isn't affected by this issue. + Thanks to Bartosz Blimke for reporting this + problem. + * This release also uses a less strict ruby version requirement for the + creation of the mswin32 native gem. +2009-05-10 (1.1.6) + * No changes. І tested native linux gems in the last release and they don't + play well with different ruby versions other than the one the gem was built + with. This release is just to bump the version number in order to skip the + native gem on rubyforge. +2009-05-10 (1.1.5) + * Started to build gems with rake-compiler gem. + * Applied patch object/array class patch from Brian Candler + and fixes. +2009-04-01 (1.1.4) + * Fixed a bug in the creation of serialized generic rails objects reported by + Friedrich Graeter . + * Deleted tests/runner.rb, we're using testrb instead. + * Editor supports Infinity in numbers now. + * Made some changes in order to get the library to compile/run under Ruby + 1.9. + * Improved speed of the code path for the fast_generate method in the pure + variant. +2008-07-10 (1.1.3) + * Wesley Beary reported a bug in json/add/core's DateTime + handling: If the nominator and denominator of the offset were divisible by + each other Ruby's Rational#to_s returns them as an integer not a fraction + with '/'. This caused a ZeroDivisionError during parsing. + * Use Date#start and DateTime#start instead of sg method, while + remaining backwards compatible. + * Supports ragel >= 6.0 now. + * Corrected some tests. + * Some minor changes. +2007-11-27 (1.1.2) + * Remember default dir (last used directory) in editor. + * JSON::Editor.edit method added, the editor can now receive json texts from + the clipboard via C-v. + * Load json texts from an URL pasted via middle button press. + * Added :create_additions option to Parser. This makes it possible to disable + the creation of additions by force, in order to treat json texts as data + while having additions loaded. + * Jacob Maine reported, that JSON(:foo) outputs a JSON + object if the rails addition is enabled, which is wrong. It now outputs a + JSON string "foo" instead, like suggested by Jacob Maine. + * Discovered a bug in the Ruby Bugs Tracker on rubyforge, that was reported + by John Evans lgastako@gmail.com. He could produce a crash in the JSON + generator by returning something other than a String instance from a + to_json method. I now guard against this by doing a rather crude type + check, which raises an exception instead of crashing. +2007-07-06 (1.1.1) + * Yui NARUSE sent some patches to fix tests for Ruby + 1.9. I applied them and adapted some of them a bit to run both on 1.8 and + 1.9. + * Introduced a JSON.parse! method without depth checking for people who like + danger. + * Made generate and pretty_generate methods configurable by an options hash. + * Added :allow_nan option to parser and generator in order to handle NaN, + Infinity, and -Infinity correctly - if requested. Floats, which aren't numbers, + aren't valid JSON according to RFC4627, so by default an exception will be + raised if any of these symbols are encountered. Thanks to Andrea Censi + for his hint about this. + * Fixed some more tests for Ruby 1.9. + * Implemented dump/load interface of Marshal as suggested in ruby-core:11405 + by murphy . + * Implemented the max_nesting feature for generate methods, too. + * Added some implementations for ruby core's custom objects for + serialisation/deserialisation purposes. +2007-05-21 (1.1.0) + * Implemented max_nesting feature for parser to avoid stack overflows for + data from untrusted sources. If you trust the source, you can disable it + with the option max_nesting => false. + * Piers Cawley reported a bug, that not every + character can be escaped by ?\ as required by RFC4627. There's a + contradiction between David Crockford's JSON checker test vectors (in + tests/fixtures) and RFC4627, though. I decided to stick to the RFC, because + the JSON checker seems to be a bit older than the RFC. + * Extended license to Ruby License, which includes the GPL. + * Added keyboard shortcuts, and 'Open location' menu item to edit_json.rb. +2007-05-09 (1.0.4) + * Applied a patch from Yui NARUSE to make JSON compile + under Ruby 1.9. Thank you very much for mailing it to me! + * Made binary variants of JSON fail early, instead of falling back to the + pure version. This should avoid overshadowing of eventual problems while + loading of the binary. +2007-03-24 (1.0.3) + * Improved performance of pure variant a bit. + * The ext variant of this release supports the mswin32 platform. Ugh! +2007-03-24 (1.0.2) + * Ext Parser didn't parse 0e0 correctly into 0.0: Fixed! +2007-03-24 (1.0.1) + * Forgot some object files in the build dir. I really like that - not! +2007-03-24 (1.0.0) + * Added C implementations for the JSON generator and a ragel based JSON + parser in C. + * Much more tests, especially fixtures from json.org. + * Further improved conformance to RFC4627. +2007-02-09 (0.4.3) + * Conform more to RFC4627 for JSON: This means JSON strings + now always must contain exactly one object "{ ... }" or array "[ ... ]" in + order to be parsed without raising an exception. The definition of what + constitutes a whitespace is narrower in JSON than in Ruby ([ \t\r\n]), and + there are differences in floats and integers (no octals or hexadecimals) as + well. + * Added aliases generate and pretty_generate of unparse and pretty_unparse. + * Fixed a test case. + * Catch an Iconv::InvalidEncoding exception, that seems to occur on some Sun + boxes with SunOS 5.8, if iconv doesn't support utf16 conversions. This was + reported by Andrew R Jackson , thanks a bunch! +2006-08-25 (0.4.2) + * Fixed a bug in handling solidi (/-characters), that was reported by + Kevin Gilpin . +2006-02-06 (0.4.1) + * Fixed a bug related to escaping with backslashes. Thanks for the report go + to Florian Munz . +2005-09-23 (0.4.0) + * Initial Rubyforge Version diff --git a/lib/mcollective/vendor/json/COPYING b/lib/mcollective/vendor/json/COPYING new file mode 100644 index 0000000..c3a2126 --- /dev/null +++ b/lib/mcollective/vendor/json/COPYING @@ -0,0 +1,58 @@ +Ruby is copyrighted free software by Yukihiro Matsumoto . +You can redistribute it and/or modify it under either the terms of the GPL +(see GPL file), or the conditions below: + + 1. You may make and give away verbatim copies of the source form of the + software without restriction, provided that you duplicate all of the + original copyright notices and associated disclaimers. + + 2. You may modify your copy of the software in any way, provided that + you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise + make them Freely Available, such as by posting said + modifications to Usenet or an equivalent medium, or by allowing + the author to include your modifications in the software. + + b) use the modified software only within your corporation or + organization. + + c) rename any non-standard executables so the names do not conflict + with standard executables, which must also be provided. + + d) make other distribution arrangements with the author. + + 3. You may distribute the software in object code or executable + form, provided that you do at least ONE of the following: + + a) distribute the executables and library files of the software, + together with instructions (in the manual page or equivalent) + on where to get the original distribution. + + b) accompany the distribution with the machine-readable source of + the software. + + c) give non-standard executables non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 4. You may modify and include the part of the software into any other + software (possibly commercial). But some files in the distribution + are not written by the author, so that they are not under this terms. + + They are gc.c(partly), utils.c(partly), regex.[ch], st.[ch] and some + files under the ./missing directory. See each file for the copying + condition. + + 5. The scripts and library files supplied as input to or produced as + output from the software do not automatically fall under the + copyright of the software, but belong to whomever generated them, + and may be sold commercially, and may be aggregated with this + software. + + 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. + diff --git a/lib/mcollective/vendor/json/COPYING-json-jruby b/lib/mcollective/vendor/json/COPYING-json-jruby new file mode 100644 index 0000000..137a3da --- /dev/null +++ b/lib/mcollective/vendor/json/COPYING-json-jruby @@ -0,0 +1,57 @@ +JSON-JRuby is copyrighted free software by Daniel Luz , +and is a derivative work of Florian Frank's json library . +You can redistribute it and/or modify it under either the terms of the GPL +version 2 (see the file GPL), or the conditions below: + + 1. You may make and give away verbatim copies of the source form of the + software without restriction, provided that you duplicate all of the + original copyright notices and associated disclaimers. + + 2. You may modify your copy of the software in any way, provided that + you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise + make them Freely Available, such as by posting said + modifications to Usenet or an equivalent medium, or by allowing + the author to include your modifications in the software. + + b) use the modified software only within your corporation or + organization. + + c) give non-standard binaries non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 3. You may distribute the software in object code or binary form, + provided that you do at least ONE of the following: + + a) distribute the binaries and library files of the software, + together with instructions (in the manual page or equivalent) + on where to get the original distribution. + + b) accompany the distribution with the machine-readable source of + the software. + + c) give non-standard binaries non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 4. You may modify and include the part of the software into any other + software (possibly commercial). But some files in the distribution + are not written by the author, so that they are not under these terms. + + For the list of those files and their copying conditions, see the + file LEGAL. + + 5. The scripts and library files supplied as input to or produced as + output from the software do not automatically fall under the + copyright of the software, but belong to whomever generated them, + and may be sold commercially, and may be aggregated with this + software. + + 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. diff --git a/lib/mcollective/vendor/json/GPL b/lib/mcollective/vendor/json/GPL new file mode 100644 index 0000000..db2fc45 --- /dev/null +++ b/lib/mcollective/vendor/json/GPL @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/lib/mcollective/vendor/json/Gemfile b/lib/mcollective/vendor/json/Gemfile new file mode 100644 index 0000000..e405da2 --- /dev/null +++ b/lib/mcollective/vendor/json/Gemfile @@ -0,0 +1,11 @@ +# vim: set ft=ruby: + +source :rubygems + +gemspec :name => 'json' +gemspec :name => 'json_pure' +gemspec :name => 'json-java' + +gem 'utils' +gem 'test-unit' +gem 'debugger', :platform => :mri_19 diff --git a/lib/mcollective/vendor/json/README-json-jruby.markdown b/lib/mcollective/vendor/json/README-json-jruby.markdown new file mode 100644 index 0000000..1336837 --- /dev/null +++ b/lib/mcollective/vendor/json/README-json-jruby.markdown @@ -0,0 +1,33 @@ +JSON-JRuby +========== + +JSON-JRuby is a port of Florian Frank's native +[`json` library](http://json.rubyforge.org/) to JRuby. +It aims to be a perfect drop-in replacement for `json_pure`. + + +Development version +=================== + +The latest version is available from the +[Git repository](http://github.com/mernen/json-jruby/tree): + + git clone git://github.com/mernen/json-jruby.git + + +Compiling +========= + +You'll need JRuby version 1.2 or greater to build JSON-JRuby. +Its path must be set on the `jruby.dir` property of +`nbproject/project.properties` (defaults to `../jruby`). + +Additionally, you'll need [Ant](http://ant.apache.org/), and +[Ragel](http://www.cs.queensu.ca/~thurston/ragel/) 6.4 or greater. + +Then, from the folder where the sources are located, type: + + ant clean jar + +to clean any leftovers from previous builds and generate the `.jar` files. +To generate a RubyGem, specify the `gem` action rather than `jar`. diff --git a/lib/mcollective/vendor/json/README.rdoc b/lib/mcollective/vendor/json/README.rdoc new file mode 100644 index 0000000..072b43d --- /dev/null +++ b/lib/mcollective/vendor/json/README.rdoc @@ -0,0 +1,358 @@ += JSON implementation for Ruby http://travis-ci.org/flori/json.png + +== Description + +This is a implementation of the JSON specification according to RFC 4627 +http://www.ietf.org/rfc/rfc4627.txt . Starting from version 1.0.0 on there +will be two variants available: + +* A pure ruby variant, that relies on the iconv and the stringscan + extensions, which are both part of the ruby standard library. +* The quite a bit faster C extension variant, which is in parts implemented + in C and comes with its own unicode conversion functions and a parser + generated by the ragel state machine compiler + http://www.cs.queensu.ca/~thurston/ragel . + +Both variants of the JSON generator generate UTF-8 character sequences by +default. If an :ascii_only option with a true value is given, they escape all +non-ASCII and control characters with \uXXXX escape sequences, and support +UTF-16 surrogate pairs in order to be able to generate the whole range of +unicode code points. + +All strings, that are to be encoded as JSON strings, should be UTF-8 byte +sequences on the Ruby side. To encode raw binary strings, that aren't UTF-8 +encoded, please use the to_json_raw_object method of String (which produces +an object, that contains a byte array) and decode the result on the receiving +endpoint. + +The JSON parsers can parse UTF-8, UTF-16BE, UTF-16LE, UTF-32BE, and UTF-32LE +JSON documents under Ruby 1.8. Under Ruby 1.9 they take advantage of Ruby's +M17n features and can parse all documents which have the correct +String#encoding set. If a document string has ASCII-8BIT as an encoding the +parser attempts to figure out which of the UTF encodings from above it is and +trys to parse it. + +== Installation + +It's recommended to use the extension variant of JSON, because it's faster than +the pure ruby variant. If you cannot build it on your system, you can settle +for the latter. + +Just type into the command line as root: + + # rake install + +The above command will build the extensions and install them on your system. + + # rake install_pure + +or + + # ruby install.rb + +will just install the pure ruby implementation of JSON. + +If you use Rubygems you can type + + # gem install json + +instead, to install the newest JSON version. + +There is also a pure ruby json only variant of the gem, that can be installed +with: + + # gem install json_pure + +== Compiling the extensions yourself + +If you want to build the extensions yourself you need rake: + + You can get it from rubyforge: + http://rubyforge.org/projects/rake + + or just type + + # gem install rake + + for the installation via rubygems. + +If you want to create the parser.c file from its parser.rl file or draw nice +graphviz images of the state machines, you need ragel from: http://www.cs.queensu.ca/~thurston/ragel + + +== Usage + +To use JSON you can + require 'json' +to load the installed variant (either the extension 'json' or the pure +variant 'json_pure'). If you have installed the extension variant, you can +pick either the extension variant or the pure variant by typing + require 'json/ext' +or + require 'json/pure' + +Now you can parse a JSON document into a ruby data structure by calling + + JSON.parse(document) + +If you want to generate a JSON document from a ruby data structure call + JSON.generate(data) + +You can also use the pretty_generate method (which formats the output more +verbosely and nicely) or fast_generate (which doesn't do any of the security +checks generate performs, e. g. nesting deepness checks). + +To create a valid JSON document you have to make sure, that the output is +embedded in either a JSON array [] or a JSON object {}. The easiest way to do +this, is by putting your values in a Ruby Array or Hash instance. + +There are also the JSON and JSON[] methods which use parse on a String or +generate a JSON document from an array or hash: + + document = JSON 'test' => 23 # => "{\"test\":23}" + document = JSON['test'] => 23 # => "{\"test\":23}" + +and + + data = JSON '{"test":23}' # => {"test"=>23} + data = JSON['{"test":23}'] # => {"test"=>23} + +You can choose to load a set of common additions to ruby core's objects if +you + require 'json/add/core' + +After requiring this you can, e. g., serialise/deserialise Ruby ranges: + + JSON JSON(1..10) # => 1..10 + +To find out how to add JSON support to other or your own classes, read the +section "More Examples" below. + +To get the best compatibility to rails' JSON implementation, you can + require 'json/add/rails' + +Both of the additions attempt to require 'json' (like above) first, if it has +not been required yet. + +== More Examples + +To create a JSON document from a ruby data structure, you can call +JSON.generate like that: + + json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10] + # => "[1,2,{\"a\":3.141},false,true,null,\"4..10\"]" + +To get back a ruby data structure from a JSON document, you have to call +JSON.parse on it: + + JSON.parse json + # => [1, 2, {"a"=>3.141}, false, true, nil, "4..10"] + +Note, that the range from the original data structure is a simple +string now. The reason for this is, that JSON doesn't support ranges +or arbitrary classes. In this case the json library falls back to call +Object#to_json, which is the same as #to_s.to_json. + +It's possible to add JSON support serialization to arbitrary classes by +simply implementing a more specialized version of the #to_json method, that +should return a JSON object (a hash converted to JSON with #to_json) like +this (don't forget the *a for all the arguments): + + class Range + def to_json(*a) + { + 'json_class' => self.class.name, # = 'Range' + 'data' => [ first, last, exclude_end? ] + }.to_json(*a) + end + end + +The hash key 'json_class' is the class, that will be asked to deserialise the +JSON representation later. In this case it's 'Range', but any namespace of +the form 'A::B' or '::A::B' will do. All other keys are arbitrary and can be +used to store the necessary data to configure the object to be deserialised. + +If a the key 'json_class' is found in a JSON object, the JSON parser checks +if the given class responds to the json_create class method. If so, it is +called with the JSON object converted to a Ruby hash. So a range can +be deserialised by implementing Range.json_create like this: + + class Range + def self.json_create(o) + new(*o['data']) + end + end + +Now it possible to serialise/deserialise ranges as well: + + json = JSON.generate [1, 2, {"a"=>3.141}, false, true, nil, 4..10] + # => "[1,2,{\"a\":3.141},false,true,null,{\"json_class\":\"Range\",\"data\":[4,10,false]}]" + JSON.parse json + # => [1, 2, {"a"=>3.141}, false, true, nil, 4..10] + +JSON.generate always creates the shortest possible string representation of a +ruby data structure in one line. This is good for data storage or network +protocols, but not so good for humans to read. Fortunately there's also +JSON.pretty_generate (or JSON.pretty_generate) that creates a more readable +output: + + puts JSON.pretty_generate([1, 2, {"a"=>3.141}, false, true, nil, 4..10]) + [ + 1, + 2, + { + "a": 3.141 + }, + false, + true, + null, + { + "json_class": "Range", + "data": [ + 4, + 10, + false + ] + } + ] + +There are also the methods Kernel#j for generate, and Kernel#jj for +pretty_generate output to the console, that work analogous to Core Ruby's p and +the pp library's pp methods. + +The script tools/server.rb contains a small example if you want to test, how +receiving a JSON object from a webrick server in your browser with the +javasript prototype library http://www.prototypejs.org works. + +== Speed Comparisons + +I have created some benchmark results (see the benchmarks/data-p4-3Ghz +subdir of the package) for the JSON-parser to estimate the speed up in the C +extension: + + Comparing times (call_time_mean): + 1 ParserBenchmarkExt#parser 900 repeats: + 553.922304770 ( real) -> 21.500x + 0.001805307 + 2 ParserBenchmarkYAML#parser 1000 repeats: + 224.513358139 ( real) -> 8.714x + 0.004454078 + 3 ParserBenchmarkPure#parser 1000 repeats: + 26.755020642 ( real) -> 1.038x + 0.037376163 + 4 ParserBenchmarkRails#parser 1000 repeats: + 25.763381731 ( real) -> 1.000x + 0.038814780 + calls/sec ( time) -> speed covers + secs/call + +In the table above 1 is JSON::Ext::Parser, 2 is YAML.load with YAML +compatbile JSON document, 3 is is JSON::Pure::Parser, and 4 is +ActiveSupport::JSON.decode. The ActiveSupport JSON-decoder converts the +input first to YAML and then uses the YAML-parser, the conversion seems to +slow it down so much that it is only as fast as the JSON::Pure::Parser! + +If you look at the benchmark data you can see that this is mostly caused by +the frequent high outliers - the median of the Rails-parser runs is still +overall smaller than the median of the JSON::Pure::Parser runs: + + Comparing times (call_time_median): + 1 ParserBenchmarkExt#parser 900 repeats: + 800.592479481 ( real) -> 26.936x + 0.001249075 + 2 ParserBenchmarkYAML#parser 1000 repeats: + 271.002390644 ( real) -> 9.118x + 0.003690004 + 3 ParserBenchmarkRails#parser 1000 repeats: + 30.227910865 ( real) -> 1.017x + 0.033082008 + 4 ParserBenchmarkPure#parser 1000 repeats: + 29.722384421 ( real) -> 1.000x + 0.033644676 + calls/sec ( time) -> speed covers + secs/call + +I have benchmarked the JSON-Generator as well. This generated a few more +values, because there are different modes that also influence the achieved +speed: + + Comparing times (call_time_mean): + 1 GeneratorBenchmarkExt#generator_fast 1000 repeats: + 547.354332608 ( real) -> 15.090x + 0.001826970 + 2 GeneratorBenchmarkExt#generator_safe 1000 repeats: + 443.968212317 ( real) -> 12.240x + 0.002252414 + 3 GeneratorBenchmarkExt#generator_pretty 900 repeats: + 375.104545883 ( real) -> 10.341x + 0.002665923 + 4 GeneratorBenchmarkPure#generator_fast 1000 repeats: + 49.978706968 ( real) -> 1.378x + 0.020008521 + 5 GeneratorBenchmarkRails#generator 1000 repeats: + 38.531868759 ( real) -> 1.062x + 0.025952543 + 6 GeneratorBenchmarkPure#generator_safe 1000 repeats: + 36.927649925 ( real) -> 1.018x 7 (>=3859) + 0.027079979 + 7 GeneratorBenchmarkPure#generator_pretty 1000 repeats: + 36.272134441 ( real) -> 1.000x 6 (>=3859) + 0.027569373 + calls/sec ( time) -> speed covers + secs/call + +In the table above 1-3 are JSON::Ext::Generator methods. 4, 6, and 7 are +JSON::Pure::Generator methods and 5 is the Rails JSON generator. It is now a +bit faster than the generator_safe and generator_pretty methods of the pure +variant but slower than the others. + +To achieve the fastest JSON document output, you can use the fast_generate +method. Beware, that this will disable the checking for circular Ruby data +structures, which may cause JSON to go into an infinite loop. + +Here are the median comparisons for completeness' sake: + + Comparing times (call_time_median): + 1 GeneratorBenchmarkExt#generator_fast 1000 repeats: + 708.258020939 ( real) -> 16.547x + 0.001411915 + 2 GeneratorBenchmarkExt#generator_safe 1000 repeats: + 569.105020353 ( real) -> 13.296x + 0.001757145 + 3 GeneratorBenchmarkExt#generator_pretty 900 repeats: + 482.825371244 ( real) -> 11.280x + 0.002071142 + 4 GeneratorBenchmarkPure#generator_fast 1000 repeats: + 62.717626652 ( real) -> 1.465x + 0.015944481 + 5 GeneratorBenchmarkRails#generator 1000 repeats: + 43.965681162 ( real) -> 1.027x + 0.022745013 + 6 GeneratorBenchmarkPure#generator_safe 1000 repeats: + 43.929073409 ( real) -> 1.026x 7 (>=3859) + 0.022763968 + 7 GeneratorBenchmarkPure#generator_pretty 1000 repeats: + 42.802514491 ( real) -> 1.000x 6 (>=3859) + 0.023363113 + calls/sec ( time) -> speed covers + secs/call + +== Author + +Florian Frank + +== License + +Ruby License, see the COPYING file included in the source distribution. The +Ruby License includes the GNU General Public License (GPL), Version 2, so see +the file GPL as well. + +== Download + +The latest version of this library can be downloaded at + +* http://rubyforge.org/frs?group_id=953 + +Online Documentation should be located at + +* http://json.rubyforge.org diff --git a/lib/mcollective/vendor/json/Rakefile b/lib/mcollective/vendor/json/Rakefile new file mode 100644 index 0000000..dedc09a --- /dev/null +++ b/lib/mcollective/vendor/json/Rakefile @@ -0,0 +1,416 @@ +begin + require 'rubygems/package_task' +rescue LoadError +end + +require 'rbconfig' +begin + include RbConfig +rescue NameError + include Config +end + + +require 'rake/clean' +CLOBBER.include Dir['benchmarks/data/*.{dat,log}'], 'doc', 'Gemfile.lock' +CLEAN.include FileList['diagrams/*.*'], 'doc', 'coverage', 'tmp', + FileList["ext/**/{Makefile,mkmf.log}"], 'build', 'dist', FileList['**/*.rbc'], + FileList["{ext,lib}/**/*.{so,bundle,#{CONFIG['DLEXT']},o,obj,pdb,lib,manifest,exp,def,jar,class,dSYM}"], + FileList['java/src/**/*.class'] + +MAKE = ENV['MAKE'] || %w[gmake make].find { |c| system(c, '-v') } +PKG_NAME = 'json' +PKG_TITLE = 'JSON Implementation for Ruby' +PKG_VERSION = File.read('VERSION').chomp +PKG_FILES = FileList["**/*"].exclude(/CVS|pkg|tmp|coverage|Makefile|\.nfs\.|\.iml\Z/).exclude(/\.(so|bundle|o|class|#{CONFIG['DLEXT']})$/) + +EXT_ROOT_DIR = 'ext/json/ext' +EXT_PARSER_DIR = "#{EXT_ROOT_DIR}/parser" +EXT_PARSER_DL = "#{EXT_PARSER_DIR}/parser.#{CONFIG['DLEXT']}" +RAGEL_PATH = "#{EXT_PARSER_DIR}/parser.rl" +EXT_PARSER_SRC = "#{EXT_PARSER_DIR}/parser.c" +PKG_FILES << EXT_PARSER_SRC +EXT_GENERATOR_DIR = "#{EXT_ROOT_DIR}/generator" +EXT_GENERATOR_DL = "#{EXT_GENERATOR_DIR}/generator.#{CONFIG['DLEXT']}" +EXT_GENERATOR_SRC = "#{EXT_GENERATOR_DIR}/generator.c" + +JAVA_DIR = "java/src/json/ext" +JAVA_RAGEL_PATH = "#{JAVA_DIR}/Parser.rl" +JAVA_PARSER_SRC = "#{JAVA_DIR}/Parser.java" +JAVA_SOURCES = FileList["#{JAVA_DIR}/*.java"] +JAVA_CLASSES = [] +JRUBY_PARSER_JAR = File.expand_path("lib/json/ext/parser.jar") +JRUBY_GENERATOR_JAR = File.expand_path("lib/json/ext/generator.jar") + +RAGEL_CODEGEN = %w[rlcodegen rlgen-cd ragel].find { |c| system(c, '-v') } +RAGEL_DOTGEN = %w[rlgen-dot rlgen-cd ragel].find { |c| system(c, '-v') } + +def myruby(*args, &block) + @myruby ||= File.join(CONFIG['bindir'], CONFIG['ruby_install_name']) + options = (Hash === args.last) ? args.pop : {} + if args.length > 1 then + sh(*([@myruby] + args + [options]), &block) + else + sh("#{@myruby} #{args.first}", options, &block) + end +end + +desc "Installing library (pure)" +task :install_pure => :version do + myruby 'install.rb' +end + +task :install_ext_really do + sitearchdir = CONFIG["sitearchdir"] + cd 'ext' do + for file in Dir["json/ext/*.#{CONFIG['DLEXT']}"] + d = File.join(sitearchdir, file) + mkdir_p File.dirname(d) + install(file, d) + end + warn " *** Installed EXT ruby library." + end +end + +desc "Installing library (extension)" +task :install_ext => [ :compile, :install_pure, :install_ext_really ] + +desc "Installing library (extension)" +if RUBY_PLATFORM =~ /java/ + task :install => :install_pure +else + task :install => :install_ext +end + +if defined?(Gem) and defined?(Gem::PackageTask) + spec_pure = Gem::Specification.new do |s| + s.name = 'json_pure' + s.version = PKG_VERSION + s.summary = PKG_TITLE + s.description = "This is a JSON implementation in pure Ruby." + + s.files = PKG_FILES + + s.require_path = 'lib' + s.add_development_dependency 'permutation' + s.add_development_dependency 'bullshit' + s.add_development_dependency 'sdoc' + s.add_development_dependency 'rake', '~>0.9.2' + s.add_dependency 'spruz', '~>0.2.8' + + s.bindir = "bin" + s.executables = [ "edit_json.rb", "prettify_json.rb" ] + + s.extra_rdoc_files << 'README.rdoc' + s.rdoc_options << + '--title' << 'JSON implemention for ruby' << '--main' << 'README.rdoc' + s.test_files.concat Dir['./tests/test_*.rb'] + + s.author = "Florian Frank" + s.email = "flori@ping.de" + s.homepage = "http://flori.github.com/#{PKG_NAME}" + s.rubyforge_project = "json" + end + + desc 'Creates a json_pure.gemspec file' + task :gemspec_pure => :version do + File.open('json_pure.gemspec', 'w') do |gemspec| + gemspec.write spec_pure.to_ruby + end + end + + Gem::PackageTask.new(spec_pure) do |pkg| + pkg.need_tar = true + pkg.package_files = PKG_FILES + end + + spec_ext = Gem::Specification.new do |s| + s.name = 'json' + s.version = PKG_VERSION + s.summary = PKG_TITLE + s.description = "This is a JSON implementation as a Ruby extension in C." + + s.files = PKG_FILES + + s.extensions = FileList['ext/**/extconf.rb'] + + s.require_path = EXT_ROOT_DIR + s.require_paths << 'ext' + s.require_paths << 'lib' + s.add_development_dependency 'permutation' + s.add_development_dependency 'bullshit' + s.add_development_dependency 'sdoc' + + s.bindir = "bin" + s.executables = [ "edit_json.rb", "prettify_json.rb" ] + + s.extra_rdoc_files << 'README.rdoc' + s.rdoc_options << + '--title' << 'JSON implemention for Ruby' << '--main' << 'README.rdoc' + s.test_files.concat Dir['./tests/test_*.rb'] + + s.author = "Florian Frank" + s.email = "flori@ping.de" + s.homepage = "http://flori.github.com/#{PKG_NAME}" + s.rubyforge_project = "json" + end + + desc 'Creates a json.gemspec file' + task :gemspec_ext => :version do + File.open('json.gemspec', 'w') do |gemspec| + gemspec.write spec_ext.to_ruby + end + end + + Gem::PackageTask.new(spec_ext) do |pkg| + pkg.need_tar = true + pkg.package_files = PKG_FILES + end + + + desc 'Create all gemspec files' + task :gemspec => [ :gemspec_pure, :gemspec_ext ] +end + +desc m = "Writing version information for #{PKG_VERSION}" +task :version do + puts m + File.open(File.join('lib', 'json', 'version.rb'), 'w') do |v| + v.puts < :clean do + ENV['JSON'] = 'pure' + ENV['RUBYOPT'] = "-Ilib #{ENV['RUBYOPT']}" + myruby '-S', 'testrb', *Dir['./tests/test_*.rb'] +end + +desc "Testing library (pure ruby and extension)" +task :test => [ :test_pure, :test_ext ] + +namespace :gems do + task :install do + sh 'bundle' + end +end + +if defined?(RUBY_ENGINE) and RUBY_ENGINE == 'jruby' + if ENV.key?('JAVA_HOME') + warn " *** JAVA_HOME was set to #{ENV['JAVA_HOME'].inspect}" + else File.directory?(local_java = '/usr/local/java/jdk') + ENV['JAVA_HOME'] = local_java + warn " *** JAVA_HOME is set to #{ENV['JAVA_HOME'].inspect}" + ENV['PATH'] = ENV['PATH'].split(/:/).unshift(java_path = "#{ENV['JAVA_HOME']}/bin") * ':' + warn " *** java binaries are assumed to be in #{java_path.inspect}" + end + + file JAVA_PARSER_SRC => JAVA_RAGEL_PATH do + cd JAVA_DIR do + if RAGEL_CODEGEN == 'ragel' + sh "ragel Parser.rl -J -o Parser.java" + else + sh "ragel -x Parser.rl | #{RAGEL_CODEGEN} -J" + end + end + end + + desc "Generate parser for java with ragel" + task :ragel => JAVA_PARSER_SRC + + desc "Delete the ragel generated Java source" + task :ragel_clean do + rm_rf JAVA_PARSER_SRC + end + + JRUBY_JAR = File.join(CONFIG["libdir"], "jruby.jar") + if File.exist?(JRUBY_JAR) + JAVA_SOURCES.each do |src| + classpath = (Dir['java/lib/*.jar'] << 'java/src' << JRUBY_JAR) * ':' + obj = src.sub(/\.java\Z/, '.class') + file obj => src do + sh 'javac', '-classpath', classpath, '-source', '1.5', src + end + JAVA_CLASSES << obj + end + else + warn "WARNING: Cannot find jruby in path => Cannot build jruby extension!" + end + + desc "Compiling jruby extension" + task :compile => JAVA_CLASSES + + desc "Package the jruby gem" + task :jruby_gem => :create_jar do + sh 'gem build json-java.gemspec' + mkdir_p 'pkg' + mv "json-#{PKG_VERSION}-java.gem", 'pkg' + end + + desc "Testing library (jruby)" + task :test_ext => :create_jar do + ENV['JSON'] = 'ext' + myruby '-S', 'testrb', '-Ilib', *Dir['./tests/test_*.rb'] + end + + file JRUBY_PARSER_JAR => :compile do + cd 'java/src' do + parser_classes = FileList[ + "json/ext/ByteListTranscoder*.class", + "json/ext/OptionsReader*.class", + "json/ext/Parser*.class", + "json/ext/RuntimeInfo*.class", + "json/ext/StringDecoder*.class", + "json/ext/Utils*.class" + ] + sh 'jar', 'cf', File.basename(JRUBY_PARSER_JAR), *parser_classes + mv File.basename(JRUBY_PARSER_JAR), File.dirname(JRUBY_PARSER_JAR) + end + end + + desc "Create parser jar" + task :create_parser_jar => JRUBY_PARSER_JAR + + file JRUBY_GENERATOR_JAR => :compile do + cd 'java/src' do + generator_classes = FileList[ + "json/ext/ByteListTranscoder*.class", + "json/ext/OptionsReader*.class", + "json/ext/Generator*.class", + "json/ext/RuntimeInfo*.class", + "json/ext/StringEncoder*.class", + "json/ext/Utils*.class" + ] + sh 'jar', 'cf', File.basename(JRUBY_GENERATOR_JAR), *generator_classes + mv File.basename(JRUBY_GENERATOR_JAR), File.dirname(JRUBY_GENERATOR_JAR) + end + end + + desc "Create generator jar" + task :create_generator_jar => JRUBY_GENERATOR_JAR + + desc "Create parser and generator jars" + task :create_jar => [ :create_parser_jar, :create_generator_jar ] + + desc "Build all gems and archives for a new release of the jruby extension." + task :build => [ :clean, :version, :jruby_gem ] + + task :release => :build +else + desc "Compiling extension" + task :compile => [ EXT_PARSER_DL, EXT_GENERATOR_DL ] + + file EXT_PARSER_DL => EXT_PARSER_SRC do + cd EXT_PARSER_DIR do + myruby 'extconf.rb' + sh MAKE + end + cp "#{EXT_PARSER_DIR}/parser.#{CONFIG['DLEXT']}", EXT_ROOT_DIR + end + + file EXT_GENERATOR_DL => EXT_GENERATOR_SRC do + cd EXT_GENERATOR_DIR do + myruby 'extconf.rb' + sh MAKE + end + cp "#{EXT_GENERATOR_DIR}/generator.#{CONFIG['DLEXT']}", EXT_ROOT_DIR + end + + desc "Testing library (extension)" + task :test_ext => :compile do + ENV['JSON'] = 'ext' + ENV['RUBYOPT'] = "-Iext:lib #{ENV['RUBYOPT']}" + myruby '-S', 'testrb', *Dir['./tests/test_*.rb'] + end + + desc "Benchmarking parser" + task :benchmark_parser do + ENV['RUBYOPT'] = "-Ilib:ext #{ENV['RUBYOPT']}" + myruby 'benchmarks/parser_benchmark.rb' + myruby 'benchmarks/parser2_benchmark.rb' + end + + desc "Benchmarking generator" + task :benchmark_generator do + ENV['RUBYOPT'] = "-Ilib:ext #{ENV['RUBYOPT']}" + myruby 'benchmarks/generator_benchmark.rb' + myruby 'benchmarks/generator2_benchmark.rb' + end + + desc "Benchmarking library" + task :benchmark => [ :benchmark_parser, :benchmark_generator ] + + desc "Create RDOC documentation" + task :doc => [ :version, EXT_PARSER_SRC ] do + sh "sdoc -o doc -t '#{PKG_TITLE}' -m README.rdoc README.rdoc lib/json.rb #{FileList['lib/json/**/*.rb']} #{EXT_PARSER_SRC} #{EXT_GENERATOR_SRC}" + end + + desc "Generate parser with ragel" + task :ragel => EXT_PARSER_SRC + + desc "Delete the ragel generated C source" + task :ragel_clean do + rm_rf EXT_PARSER_SRC + end + + file EXT_PARSER_SRC => RAGEL_PATH do + cd EXT_PARSER_DIR do + if RAGEL_CODEGEN == 'ragel' + sh "ragel parser.rl -G2 -o parser.c" + else + sh "ragel -x parser.rl | #{RAGEL_CODEGEN} -G2" + end + src = File.read("parser.c").gsub(/[ \t]+$/, '') + File.open("parser.c", "w") {|f| f.print src} + end + end + + desc "Generate diagrams of ragel parser (ps)" + task :ragel_dot_ps do + root = 'diagrams' + specs = [] + File.new(RAGEL_PATH).grep(/^\s*machine\s*(\S+);\s*$/) { specs << $1 } + for s in specs + if RAGEL_DOTGEN == 'ragel' + sh "ragel #{RAGEL_PATH} -S#{s} -p -V | dot -Tps -o#{root}/#{s}.ps" + else + sh "ragel -x #{RAGEL_PATH} -S#{s} | #{RAGEL_DOTGEN} -p|dot -Tps -o#{root}/#{s}.ps" + end + end + end + + desc "Generate diagrams of ragel parser (png)" + task :ragel_dot_png do + root = 'diagrams' + specs = [] + File.new(RAGEL_PATH).grep(/^\s*machine\s*(\S+);\s*$/) { specs << $1 } + for s in specs + if RAGEL_DOTGEN == 'ragel' + sh "ragel #{RAGEL_PATH} -S#{s} -p -V | dot -Tpng -o#{root}/#{s}.png" + else + sh "ragel -x #{RAGEL_PATH} -S#{s} | #{RAGEL_DOTGEN} -p|dot -Tpng -o#{root}/#{s}.png" + end + end + end + + desc "Generate diagrams of ragel parser" + task :ragel_dot => [ :ragel_dot_png, :ragel_dot_ps ] + + desc "Build all gems and archives for a new release of json and json_pure." + task :build => [ :clean, :gemspec, :package ] + + task :release => :build +end + +desc "Compile in the the source directory" +task :default => [ :clean, :gemspec, :test ] diff --git a/lib/mcollective/vendor/json/TODO b/lib/mcollective/vendor/json/TODO new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/mcollective/vendor/json/TODO @@ -0,0 +1 @@ + diff --git a/lib/mcollective/vendor/json/VERSION b/lib/mcollective/vendor/json/VERSION new file mode 100644 index 0000000..9075be4 --- /dev/null +++ b/lib/mcollective/vendor/json/VERSION @@ -0,0 +1 @@ +1.5.5 diff --git a/lib/mcollective/vendor/json/bin/edit_json.rb b/lib/mcollective/vendor/json/bin/edit_json.rb new file mode 100755 index 0000000..04a8189 --- /dev/null +++ b/lib/mcollective/vendor/json/bin/edit_json.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby +require 'json/editor' + +filename, encoding = ARGV +JSON::Editor.start(encoding) do |window| + if filename + window.file_open(filename) + end +end diff --git a/lib/mcollective/vendor/json/bin/prettify_json.rb b/lib/mcollective/vendor/json/bin/prettify_json.rb new file mode 100755 index 0000000..3c53183 --- /dev/null +++ b/lib/mcollective/vendor/json/bin/prettify_json.rb @@ -0,0 +1,48 @@ +#!/usr/bin/env ruby + +require 'json' +require 'fileutils' +include FileUtils +require 'spruz/go' +include Spruz::GO + +opts = go 'slhi:', args = ARGV.dup +if opts['h'] || opts['l'] && opts['s'] + puts < false, :create_additions => false } + +document = + if filename = args.first or filename == '-' + File.read(filename) + else + STDIN.read + end + +json = JSON.parse document, json_opts + +output = if opts['s'] + JSON.fast_generate json, json_opts +else # default is -l + JSON.pretty_generate json, json_opts +end + +if opts['i'] && filename + cp filename, "#{filename}.#{opts['i']}" + File.open(filename, 'w') { |f| f.puts output } +else + puts output +end diff --git a/lib/mcollective/vendor/json/data/example.json b/lib/mcollective/vendor/json/data/example.json new file mode 100644 index 0000000..88b4e82 --- /dev/null +++ b/lib/mcollective/vendor/json/data/example.json @@ -0,0 +1 @@ +{"a":2,"b":3.141,"TIME":"2007-03-14T11:52:40","c":"c","d":[1,"b",3.14],"COUNT":666,"e":{"foo":"bar"},"foo":"B\u00e4r","g":"\u677e\u672c\u884c\u5f18","h":1000.0,"bar":"\u00a9 \u2260 \u20ac!","i":0.001,"j":"\ud840\udc01"} diff --git a/lib/mcollective/vendor/json/data/index.html b/lib/mcollective/vendor/json/data/index.html new file mode 100644 index 0000000..abe6fdb --- /dev/null +++ b/lib/mcollective/vendor/json/data/index.html @@ -0,0 +1,38 @@ + + + + Javascript Example + + + + + +

Fetching object from server

+
+ Wait...
+ +
+ + + diff --git a/lib/mcollective/vendor/json/data/prototype.js b/lib/mcollective/vendor/json/data/prototype.js new file mode 100644 index 0000000..5c73462 --- /dev/null +++ b/lib/mcollective/vendor/json/data/prototype.js @@ -0,0 +1,4184 @@ +/* Prototype JavaScript framework, version 1.6.0 + * (c) 2005-2007 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://www.prototypejs.org/ + * + *--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.6.0', + + Browser: { + IE: !!(window.attachEvent && !window.opera), + Opera: !!window.opera, + WebKit: navigator.userAgent.indexOf('AppleWebKit/') > -1, + Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1, + MobileSafari: !!navigator.userAgent.match(/Apple.*Mobile.*Safari/) + }, + + BrowserFeatures: { + XPath: !!document.evaluate, + ElementExtensions: !!window.HTMLElement, + SpecificElementExtensions: + document.createElement('div').__proto__ && + document.createElement('div').__proto__ !== + document.createElement('form').__proto__ + }, + + ScriptFragment: ']*>([\\S\\s]*?)<\/script>', + JSONFilter: /^\/\*-secure-([\s\S]*)\*\/\s*$/, + + emptyFunction: function() { }, + K: function(x) { return x } +}; + +if (Prototype.Browser.MobileSafari) + Prototype.BrowserFeatures.SpecificElementExtensions = false; + +if (Prototype.Browser.WebKit) + Prototype.BrowserFeatures.XPath = false; + +/* Based on Alex Arnell's inheritance implementation. */ +var Class = { + create: function() { + var parent = null, properties = $A(arguments); + if (Object.isFunction(properties[0])) + parent = properties.shift(); + + function klass() { + this.initialize.apply(this, arguments); + } + + Object.extend(klass, Class.Methods); + klass.superclass = parent; + klass.subclasses = []; + + if (parent) { + var subclass = function() { }; + subclass.prototype = parent.prototype; + klass.prototype = new subclass; + parent.subclasses.push(klass); + } + + for (var i = 0; i < properties.length; i++) + klass.addMethods(properties[i]); + + if (!klass.prototype.initialize) + klass.prototype.initialize = Prototype.emptyFunction; + + klass.prototype.constructor = klass; + + return klass; + } +}; + +Class.Methods = { + addMethods: function(source) { + var ancestor = this.superclass && this.superclass.prototype; + var properties = Object.keys(source); + + if (!Object.keys({ toString: true }).length) + properties.push("toString", "valueOf"); + + for (var i = 0, length = properties.length; i < length; i++) { + var property = properties[i], value = source[property]; + if (ancestor && Object.isFunction(value) && + value.argumentNames().first() == "$super") { + var method = value, value = Object.extend((function(m) { + return function() { return ancestor[m].apply(this, arguments) }; + })(property).wrap(method), { + valueOf: function() { return method }, + toString: function() { return method.toString() } + }); + } + this.prototype[property] = value; + } + + return this; + } +}; + +var Abstract = { }; + +Object.extend = function(destination, source) { + for (var property in source) + destination[property] = source[property]; + return destination; +}; + +Object.extend(Object, { + inspect: function(object) { + try { + if (object === undefined) return 'undefined'; + if (object === null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } + }, + + toJSON: function(object) { + var type = typeof object; + switch (type) { + case 'undefined': + case 'function': + case 'unknown': return; + case 'boolean': return object.toString(); + } + + if (object === null) return 'null'; + if (object.toJSON) return object.toJSON(); + if (Object.isElement(object)) return; + + var results = []; + for (var property in object) { + var value = Object.toJSON(object[property]); + if (value !== undefined) + results.push(property.toJSON() + ': ' + value); + } + + return '{' + results.join(', ') + '}'; + }, + + toQueryString: function(object) { + return $H(object).toQueryString(); + }, + + toHTML: function(object) { + return object && object.toHTML ? object.toHTML() : String.interpret(object); + }, + + keys: function(object) { + var keys = []; + for (var property in object) + keys.push(property); + return keys; + }, + + values: function(object) { + var values = []; + for (var property in object) + values.push(object[property]); + return values; + }, + + clone: function(object) { + return Object.extend({ }, object); + }, + + isElement: function(object) { + return object && object.nodeType == 1; + }, + + isArray: function(object) { + return object && object.constructor === Array; + }, + + isHash: function(object) { + return object instanceof Hash; + }, + + isFunction: function(object) { + return typeof object == "function"; + }, + + isString: function(object) { + return typeof object == "string"; + }, + + isNumber: function(object) { + return typeof object == "number"; + }, + + isUndefined: function(object) { + return typeof object == "undefined"; + } +}); + +Object.extend(Function.prototype, { + argumentNames: function() { + var names = this.toString().match(/^[\s\(]*function[^(]*\((.*?)\)/)[1].split(",").invoke("strip"); + return names.length == 1 && !names[0] ? [] : names; + }, + + bind: function() { + if (arguments.length < 2 && arguments[0] === undefined) return this; + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } + }, + + bindAsEventListener: function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function(event) { + return __method.apply(object, [event || window.event].concat(args)); + } + }, + + curry: function() { + if (!arguments.length) return this; + var __method = this, args = $A(arguments); + return function() { + return __method.apply(this, args.concat($A(arguments))); + } + }, + + delay: function() { + var __method = this, args = $A(arguments), timeout = args.shift() * 1000; + return window.setTimeout(function() { + return __method.apply(__method, args); + }, timeout); + }, + + wrap: function(wrapper) { + var __method = this; + return function() { + return wrapper.apply(this, [__method.bind(this)].concat($A(arguments))); + } + }, + + methodize: function() { + if (this._methodized) return this._methodized; + var __method = this; + return this._methodized = function() { + return __method.apply(null, [this].concat($A(arguments))); + }; + } +}); + +Function.prototype.defer = Function.prototype.delay.curry(0.01); + +Date.prototype.toJSON = function() { + return '"' + this.getUTCFullYear() + '-' + + (this.getUTCMonth() + 1).toPaddedString(2) + '-' + + this.getUTCDate().toPaddedString(2) + 'T' + + this.getUTCHours().toPaddedString(2) + ':' + + this.getUTCMinutes().toPaddedString(2) + ':' + + this.getUTCSeconds().toPaddedString(2) + 'Z"'; +}; + +var Try = { + these: function() { + var returnValue; + + for (var i = 0, length = arguments.length; i < length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) { } + } + + return returnValue; + } +}; + +RegExp.prototype.match = RegExp.prototype.test; + +RegExp.escape = function(str) { + return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); +}; + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create({ + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + this.timer = setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + execute: function() { + this.callback(this); + }, + + stop: function() { + if (!this.timer) return; + clearInterval(this.timer); + this.timer = null; + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.execute(); + } finally { + this.currentlyExecuting = false; + } + } + } +}); +Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } +}); + +Object.extend(String.prototype, { + gsub: function(pattern, replacement) { + var result = '', source = this, match; + replacement = arguments.callee.prepareReplacement(replacement); + + while (source.length > 0) { + if (match = source.match(pattern)) { + result += source.slice(0, match.index); + result += String.interpret(replacement(match)); + source = source.slice(match.index + match[0].length); + } else { + result += source, source = ''; + } + } + return result; + }, + + sub: function(pattern, replacement, count) { + replacement = this.gsub.prepareReplacement(replacement); + count = count === undefined ? 1 : count; + + return this.gsub(pattern, function(match) { + if (--count < 0) return match[0]; + return replacement(match); + }); + }, + + scan: function(pattern, iterator) { + this.gsub(pattern, iterator); + return String(this); + }, + + truncate: function(length, truncation) { + length = length || 30; + truncation = truncation === undefined ? '...' : truncation; + return this.length > length ? + this.slice(0, length - truncation.length) + truncation : String(this); + }, + + strip: function() { + return this.replace(/^\s+/, '').replace(/\s+$/, ''); + }, + + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(function(script) { return eval(script) }); + }, + + escapeHTML: function() { + var self = arguments.callee; + self.text.data = this; + return self.div.innerHTML; + }, + + unescapeHTML: function() { + var div = new Element('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? (div.childNodes.length > 1 ? + $A(div.childNodes).inject('', function(memo, node) { return memo+node.nodeValue }) : + div.childNodes[0].nodeValue) : ''; + }, + + toQueryParams: function(separator) { + var match = this.strip().match(/([^?#]*)(#.*)?$/); + if (!match) return { }; + + return match[1].split(separator || '&').inject({ }, function(hash, pair) { + if ((pair = pair.split('='))[0]) { + var key = decodeURIComponent(pair.shift()); + var value = pair.length > 1 ? pair.join('=') : pair[0]; + if (value != undefined) value = decodeURIComponent(value); + + if (key in hash) { + if (!Object.isArray(hash[key])) hash[key] = [hash[key]]; + hash[key].push(value); + } + else hash[key] = value; + } + return hash; + }); + }, + + toArray: function() { + return this.split(''); + }, + + succ: function() { + return this.slice(0, this.length - 1) + + String.fromCharCode(this.charCodeAt(this.length - 1) + 1); + }, + + times: function(count) { + return count < 1 ? '' : new Array(count + 1).join(this); + }, + + camelize: function() { + var parts = this.split('-'), len = parts.length; + if (len == 1) return parts[0]; + + var camelized = this.charAt(0) == '-' + ? parts[0].charAt(0).toUpperCase() + parts[0].substring(1) + : parts[0]; + + for (var i = 1; i < len; i++) + camelized += parts[i].charAt(0).toUpperCase() + parts[i].substring(1); + + return camelized; + }, + + capitalize: function() { + return this.charAt(0).toUpperCase() + this.substring(1).toLowerCase(); + }, + + underscore: function() { + return this.gsub(/::/, '/').gsub(/([A-Z]+)([A-Z][a-z])/,'#{1}_#{2}').gsub(/([a-z\d])([A-Z])/,'#{1}_#{2}').gsub(/-/,'_').toLowerCase(); + }, + + dasherize: function() { + return this.gsub(/_/,'-'); + }, + + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + + isJSON: function() { + var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + }, + + evalJSON: function(sanitize) { + var json = this.unfilterJSON(); + try { + if (!sanitize || json.isJSON()) return eval('(' + json + ')'); + } catch (e) { } + throw new SyntaxError('Badly formed JSON string: ' + this.inspect()); + }, + + include: function(pattern) { + return this.indexOf(pattern) > -1; + }, + + startsWith: function(pattern) { + return this.indexOf(pattern) === 0; + }, + + endsWith: function(pattern) { + var d = this.length - pattern.length; + return d >= 0 && this.lastIndexOf(pattern) === d; + }, + + empty: function() { + return this == ''; + }, + + blank: function() { + return /^\s*$/.test(this); + }, + + interpolate: function(object, pattern) { + return new Template(this, pattern).evaluate(object); + } +}); + +if (Prototype.Browser.WebKit || Prototype.Browser.IE) Object.extend(String.prototype, { + escapeHTML: function() { + return this.replace(/&/g,'&').replace(//g,'>'); + }, + unescapeHTML: function() { + return this.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); + } +}); + +String.prototype.gsub.prepareReplacement = function(replacement) { + if (Object.isFunction(replacement)) return replacement; + var template = new Template(replacement); + return function(match) { return template.evaluate(match) }; +}; + +String.prototype.parseQuery = String.prototype.toQueryParams; + +Object.extend(String.prototype.escapeHTML, { + div: document.createElement('div'), + text: document.createTextNode('') +}); + +with (String.prototype.escapeHTML) div.appendChild(text); + +var Template = Class.create({ + initialize: function(template, pattern) { + this.template = template.toString(); + this.pattern = pattern || Template.Pattern; + }, + + evaluate: function(object) { + if (Object.isFunction(object.toTemplateReplacements)) + object = object.toTemplateReplacements(); + + return this.template.gsub(this.pattern, function(match) { + if (object == null) return ''; + + var before = match[1] || ''; + if (before == '\\') return match[2]; + + var ctx = object, expr = match[3]; + var pattern = /^([^.[]+|\[((?:.*?[^\\])?)\])(\.|\[|$)/, match = pattern.exec(expr); + if (match == null) return before; + + while (match != null) { + var comp = match[1].startsWith('[') ? match[2].gsub('\\\\]', ']') : match[1]; + ctx = ctx[comp]; + if (null == ctx || '' == match[3]) break; + expr = expr.substring('[' == match[3] ? match[1].length : match[0].length); + match = pattern.exec(expr); + } + + return before + String.interpret(ctx); + }.bind(this)); + } +}); +Template.Pattern = /(^|.|\r|\n)(#\{(.*?)\})/; + +var $break = { }; + +var Enumerable = { + each: function(iterator, context) { + var index = 0; + iterator = iterator.bind(context); + try { + this._each(function(value) { + iterator(value, index++); + }); + } catch (e) { + if (e != $break) throw e; + } + return this; + }, + + eachSlice: function(number, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var index = -number, slices = [], array = this.toArray(); + while ((index += number) < array.length) + slices.push(array.slice(index, index+number)); + return slices.collect(iterator, context); + }, + + all: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = true; + this.each(function(value, index) { + result = result && !!iterator(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result = false; + this.each(function(value, index) { + if (result = !!iterator(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function(iterator, context) { + iterator = iterator.bind(context); + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(filter, iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var results = []; + + if (Object.isString(filter)) + filter = new RegExp(filter); + + this.each(function(value, index) { + if (filter.match(value)) + results.push(iterator(value, index)); + }); + return results; + }, + + include: function(object) { + if (Object.isFunction(this.indexOf)) + if (this.indexOf(object) != -1) return true; + + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inGroupsOf: function(number, fillWith) { + fillWith = fillWith === undefined ? null : fillWith; + return this.eachSlice(number, function(slice) { + while(slice.length < number) slice.push(fillWith); + return slice; + }); + }, + + inject: function(memo, iterator, context) { + iterator = iterator.bind(context); + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.map(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == undefined || value >= result) + result = value; + }); + return result; + }, + + min: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var result; + this.each(function(value, index) { + value = iterator(value, index); + if (result == undefined || value < result) + result = value; + }); + return result; + }, + + partition: function(iterator, context) { + iterator = iterator ? iterator.bind(context) : Prototype.K; + var trues = [], falses = []; + this.each(function(value, index) { + (iterator(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator, context) { + iterator = iterator.bind(context); + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator, context) { + iterator = iterator.bind(context); + return this.map(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.map(); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (Object.isFunction(args.last())) + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + return iterator(collections.pluck(index)); + }); + }, + + size: function() { + return this.toArray().length; + }, + + inspect: function() { + return '#'; + } +}; + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + filter: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray, + every: Enumerable.all, + some: Enumerable.any +}); +function $A(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; +} + +if (Prototype.Browser.WebKit) { + function $A(iterable) { + if (!iterable) return []; + if (!(Object.isFunction(iterable) && iterable == '[object NodeList]') && + iterable.toArray) return iterable.toArray(); + var length = iterable.length, results = new Array(length); + while (length--) results[length] = iterable[length]; + return results; + } +} + +Array.from = $A; + +Object.extend(Array.prototype, Enumerable); + +if (!Array.prototype._reverse) Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0, length = this.length; i < length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(Object.isArray(value) ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + reduce: function() { + return this.length > 1 ? this : this[0]; + }, + + uniq: function(sorted) { + return this.inject([], function(array, value, index) { + if (0 == index || (sorted ? array.last() != value : !array.include(value))) + array.push(value); + return array; + }); + }, + + intersect: function(array) { + return this.uniq().findAll(function(item) { + return array.detect(function(value) { return item === value }); + }); + }, + + clone: function() { + return [].concat(this); + }, + + size: function() { + return this.length; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + }, + + toJSON: function() { + var results = []; + this.each(function(object) { + var value = Object.toJSON(object); + if (value !== undefined) results.push(value); + }); + return '[' + results.join(', ') + ']'; + } +}); + +// use native browser JS 1.6 implementation if available +if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + +if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + i || (i = 0); + var length = this.length; + if (i < 0) i = length + i; + for (; i < length; i++) + if (this[i] === item) return i; + return -1; +}; + +if (!Array.prototype.lastIndexOf) Array.prototype.lastIndexOf = function(item, i) { + i = isNaN(i) ? this.length : (i < 0 ? this.length + i : i) + 1; + var n = this.slice(0, i).reverse().indexOf(item); + return (n < 0) ? n : i - n - 1; +}; + +Array.prototype.toArray = Array.prototype.clone; + +function $w(string) { + if (!Object.isString(string)) return []; + string = string.strip(); + return string ? string.split(/\s+/) : []; +} + +if (Prototype.Browser.Opera){ + Array.prototype.concat = function() { + var array = []; + for (var i = 0, length = this.length; i < length; i++) array.push(this[i]); + for (var i = 0, length = arguments.length; i < length; i++) { + if (Object.isArray(arguments[i])) { + for (var j = 0, arrayLength = arguments[i].length; j < arrayLength; j++) + array.push(arguments[i][j]); + } else { + array.push(arguments[i]); + } + } + return array; + }; +} +Object.extend(Number.prototype, { + toColorPart: function() { + return this.toPaddedString(2, 16); + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + }, + + toPaddedString: function(length, radix) { + var string = this.toString(radix || 10); + return '0'.times(length - string.length) + string; + }, + + toJSON: function() { + return isFinite(this) ? this.toString() : 'null'; + } +}); + +$w('abs round ceil floor').each(function(method){ + Number.prototype[method] = Math[method].methodize(); +}); +function $H(object) { + return new Hash(object); +}; + +var Hash = Class.create(Enumerable, (function() { + if (function() { + var i = 0, Test = function(value) { this.key = value }; + Test.prototype.key = 'foo'; + for (var property in new Test('bar')) i++; + return i > 1; + }()) { + function each(iterator) { + var cache = []; + for (var key in this._object) { + var value = this._object[key]; + if (cache.include(key)) continue; + cache.push(key); + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + } + } else { + function each(iterator) { + for (var key in this._object) { + var value = this._object[key], pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + } + } + + function toQueryPair(key, value) { + if (Object.isUndefined(value)) return key; + return key + '=' + encodeURIComponent(String.interpret(value)); + } + + return { + initialize: function(object) { + this._object = Object.isHash(object) ? object.toObject() : Object.clone(object); + }, + + _each: each, + + set: function(key, value) { + return this._object[key] = value; + }, + + get: function(key) { + return this._object[key]; + }, + + unset: function(key) { + var value = this._object[key]; + delete this._object[key]; + return value; + }, + + toObject: function() { + return Object.clone(this._object); + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + index: function(value) { + var match = this.detect(function(pair) { + return pair.value === value; + }); + return match && match.key; + }, + + merge: function(object) { + return this.clone().update(object); + }, + + update: function(object) { + return new Hash(object).inject(this, function(result, pair) { + result.set(pair.key, pair.value); + return result; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + var key = encodeURIComponent(pair.key), values = pair.value; + + if (values && typeof values == 'object') { + if (Object.isArray(values)) + return values.map(toQueryPair.curry(key)).join('&'); + } + return toQueryPair(key, values); + }).join('&'); + }, + + inspect: function() { + return '#'; + }, + + toJSON: function() { + return Object.toJSON(this.toObject()); + }, + + clone: function() { + return new Hash(this); + } + } +})()); + +Hash.prototype.toTemplateReplacements = Hash.prototype.toObject; +Hash.from = $H; +var ObjectRange = Class.create(Enumerable, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + while (this.include(value)) { + iterator(value); + value = value.succ(); + } + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +}; + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new XMLHttpRequest()}, + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')} + ) || false; + }, + + activeRequestCount: 0 +}; + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responder) { + if (!this.include(responder)) + this.responders.push(responder); + }, + + unregister: function(responder) { + this.responders = this.responders.without(responder); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (Object.isFunction(responder[callback])) { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) { } + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { Ajax.activeRequestCount++ }, + onComplete: function() { Ajax.activeRequestCount-- } +}); + +Ajax.Base = Class.create({ + initialize: function(options) { + this.options = { + method: 'post', + asynchronous: true, + contentType: 'application/x-www-form-urlencoded', + encoding: 'UTF-8', + parameters: '', + evalJSON: true, + evalJS: true + }; + Object.extend(this.options, options || { }); + + this.options.method = this.options.method.toLowerCase(); + if (Object.isString(this.options.parameters)) + this.options.parameters = this.options.parameters.toQueryParams(); + } +}); + +Ajax.Request = Class.create(Ajax.Base, { + _complete: false, + + initialize: function($super, url, options) { + $super(options); + this.transport = Ajax.getTransport(); + this.request(url); + }, + + request: function(url) { + this.url = url; + this.method = this.options.method; + var params = Object.clone(this.options.parameters); + + if (!['get', 'post'].include(this.method)) { + // simulate other verbs over post + params['_method'] = this.method; + this.method = 'post'; + } + + this.parameters = params; + + if (params = Object.toQueryString(params)) { + // when GET, append parameters to URL + if (this.method == 'get') + this.url += (this.url.include('?') ? '&' : '?') + params; + else if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) + params += '&_='; + } + + try { + var response = new Ajax.Response(this); + if (this.options.onCreate) this.options.onCreate(response); + Ajax.Responders.dispatch('onCreate', this, response); + + this.transport.open(this.method.toUpperCase(), this.url, + this.options.asynchronous); + + if (this.options.asynchronous) this.respondToReadyState.bind(this).defer(1); + + this.transport.onreadystatechange = this.onStateChange.bind(this); + this.setRequestHeaders(); + + this.body = this.method == 'post' ? (this.options.postBody || params) : null; + this.transport.send(this.body); + + /* Force Firefox to handle ready state 4 for synchronous requests */ + if (!this.options.asynchronous && this.transport.overrideMimeType) + this.onStateChange(); + + } + catch (e) { + this.dispatchException(e); + } + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState > 1 && !((readyState == 4) && this._complete)) + this.respondToReadyState(this.transport.readyState); + }, + + setRequestHeaders: function() { + var headers = { + 'X-Requested-With': 'XMLHttpRequest', + 'X-Prototype-Version': Prototype.Version, + 'Accept': 'text/javascript, text/html, application/xml, text/xml, */*' + }; + + if (this.method == 'post') { + headers['Content-type'] = this.options.contentType + + (this.options.encoding ? '; charset=' + this.options.encoding : ''); + + /* Force "Connection: close" for older Mozilla browsers to work + * around a bug where XMLHttpRequest sends an incorrect + * Content-length header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType && + (navigator.userAgent.match(/Gecko\/(\d{4})/) || [0,2005])[1] < 2005) + headers['Connection'] = 'close'; + } + + // user-defined headers + if (typeof this.options.requestHeaders == 'object') { + var extras = this.options.requestHeaders; + + if (Object.isFunction(extras.push)) + for (var i = 0, length = extras.length; i < length; i += 2) + headers[extras[i]] = extras[i+1]; + else + $H(extras).each(function(pair) { headers[pair.key] = pair.value }); + } + + for (var name in headers) + this.transport.setRequestHeader(name, headers[name]); + }, + + success: function() { + var status = this.getStatus(); + return !status || (status >= 200 && status < 300); + }, + + getStatus: function() { + try { + return this.transport.status || 0; + } catch (e) { return 0 } + }, + + respondToReadyState: function(readyState) { + var state = Ajax.Request.Events[readyState], response = new Ajax.Response(this); + + if (state == 'Complete') { + try { + this._complete = true; + (this.options['on' + response.status] + || this.options['on' + (this.success() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + var contentType = response.getHeader('Content-type'); + if (this.options.evalJS == 'force' + || (this.options.evalJS && contentType + && contentType.match(/^\s*(text|application)\/(x-)?(java|ecma)script(;.*)?\s*$/i))) + this.evalResponse(); + } + + try { + (this.options['on' + state] || Prototype.emptyFunction)(response, response.headerJSON); + Ajax.Responders.dispatch('on' + state, this, response, response.headerJSON); + } catch (e) { + this.dispatchException(e); + } + + if (state == 'Complete') { + // avoid memory leak in MSIE: clean up + this.transport.onreadystatechange = Prototype.emptyFunction; + } + }, + + getHeader: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) { return null } + }, + + evalResponse: function() { + try { + return eval((this.transport.responseText || '').unfilterJSON()); + } catch (e) { + this.dispatchException(e); + } + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Response = Class.create({ + initialize: function(request){ + this.request = request; + var transport = this.transport = request.transport, + readyState = this.readyState = transport.readyState; + + if((readyState > 2 && !Prototype.Browser.IE) || readyState == 4) { + this.status = this.getStatus(); + this.statusText = this.getStatusText(); + this.responseText = String.interpret(transport.responseText); + this.headerJSON = this._getHeaderJSON(); + } + + if(readyState == 4) { + var xml = transport.responseXML; + this.responseXML = xml === undefined ? null : xml; + this.responseJSON = this._getResponseJSON(); + } + }, + + status: 0, + statusText: '', + + getStatus: Ajax.Request.prototype.getStatus, + + getStatusText: function() { + try { + return this.transport.statusText || ''; + } catch (e) { return '' } + }, + + getHeader: Ajax.Request.prototype.getHeader, + + getAllHeaders: function() { + try { + return this.getAllResponseHeaders(); + } catch (e) { return null } + }, + + getResponseHeader: function(name) { + return this.transport.getResponseHeader(name); + }, + + getAllResponseHeaders: function() { + return this.transport.getAllResponseHeaders(); + }, + + _getHeaderJSON: function() { + var json = this.getHeader('X-JSON'); + if (!json) return null; + json = decodeURIComponent(escape(json)); + try { + return json.evalJSON(this.request.options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + }, + + _getResponseJSON: function() { + var options = this.request.options; + if (!options.evalJSON || (options.evalJSON != 'force' && + !(this.getHeader('Content-type') || '').include('application/json'))) + return null; + try { + return this.transport.responseText.evalJSON(options.sanitizeJSON); + } catch (e) { + this.request.dispatchException(e); + } + } +}); + +Ajax.Updater = Class.create(Ajax.Request, { + initialize: function($super, container, url, options) { + this.container = { + success: (container.success || container), + failure: (container.failure || (container.success ? null : container)) + }; + + options = options || { }; + var onComplete = options.onComplete; + options.onComplete = (function(response, param) { + this.updateContent(response.responseText); + if (Object.isFunction(onComplete)) onComplete(response, param); + }).bind(this); + + $super(url, options); + }, + + updateContent: function(responseText) { + var receiver = this.container[this.success() ? 'success' : 'failure'], + options = this.options; + + if (!options.evalScripts) responseText = responseText.stripScripts(); + + if (receiver = $(receiver)) { + if (options.insertion) { + if (Object.isString(options.insertion)) { + var insertion = { }; insertion[options.insertion] = responseText; + receiver.insert(insertion); + } + else options.insertion(receiver, responseText); + } + else receiver.update(responseText); + } + + if (this.success()) { + if (this.onComplete) this.onComplete.bind(this).defer(); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(Ajax.Base, { + initialize: function($super, container, url, options) { + $super(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = { }; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.options.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(response) { + if (this.options.decay) { + this.decay = (response.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = response.responseText; + } + this.timer = this.onTimerEvent.bind(this).delay(this.decay * this.frequency); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +function $(element) { + if (arguments.length > 1) { + for (var i = 0, elements = [], length = arguments.length; i < length; i++) + elements.push($(arguments[i])); + return elements; + } + if (Object.isString(element)) + element = document.getElementById(element); + return Element.extend(element); +} + +if (Prototype.BrowserFeatures.XPath) { + document._getElementsByXPath = function(expression, parentElement) { + var results = []; + var query = document.evaluate(expression, $(parentElement) || document, + null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + for (var i = 0, length = query.snapshotLength; i < length; i++) + results.push(Element.extend(query.snapshotItem(i))); + return results; + }; +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Node) var Node = { }; + +if (!Node.ELEMENT_NODE) { + // DOM level 2 ECMAScript Language Binding + Object.extend(Node, { + ELEMENT_NODE: 1, + ATTRIBUTE_NODE: 2, + TEXT_NODE: 3, + CDATA_SECTION_NODE: 4, + ENTITY_REFERENCE_NODE: 5, + ENTITY_NODE: 6, + PROCESSING_INSTRUCTION_NODE: 7, + COMMENT_NODE: 8, + DOCUMENT_NODE: 9, + DOCUMENT_TYPE_NODE: 10, + DOCUMENT_FRAGMENT_NODE: 11, + NOTATION_NODE: 12 + }); +} + +(function() { + var element = this.Element; + this.Element = function(tagName, attributes) { + attributes = attributes || { }; + tagName = tagName.toLowerCase(); + var cache = Element.cache; + if (Prototype.Browser.IE && attributes.name) { + tagName = '<' + tagName + ' name="' + attributes.name + '">'; + delete attributes.name; + return Element.writeAttribute(document.createElement(tagName), attributes); + } + if (!cache[tagName]) cache[tagName] = Element.extend(document.createElement(tagName)); + return Element.writeAttribute(cache[tagName].cloneNode(false), attributes); + }; + Object.extend(this.Element, element || { }); +}).call(window); + +Element.cache = { }; + +Element.Methods = { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function(element) { + element = $(element); + Element[Element.visible(element) ? 'hide' : 'show'](element); + return element; + }, + + hide: function(element) { + $(element).style.display = 'none'; + return element; + }, + + show: function(element) { + $(element).style.display = ''; + return element; + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + return element; + }, + + update: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + content = Object.toHTML(content); + element.innerHTML = content.stripScripts(); + content.evalScripts.bind(content).defer(); + return element; + }, + + replace: function(element, content) { + element = $(element); + if (content && content.toElement) content = content.toElement(); + else if (!Object.isElement(content)) { + content = Object.toHTML(content); + var range = element.ownerDocument.createRange(); + range.selectNode(element); + content.evalScripts.bind(content).defer(); + content = range.createContextualFragment(content.stripScripts()); + } + element.parentNode.replaceChild(content, element); + return element; + }, + + insert: function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = {bottom:insertions}; + + var content, t, range; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + t = Element._insertionTranslations[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + t.insert(element, content); + continue; + } + + content = Object.toHTML(content); + + range = element.ownerDocument.createRange(); + t.initializeRange(element, range); + t.insert(element, range.createContextualFragment(content.stripScripts())); + + content.evalScripts.bind(content).defer(); + } + + return element; + }, + + wrap: function(element, wrapper, attributes) { + element = $(element); + if (Object.isElement(wrapper)) + $(wrapper).writeAttribute(attributes || { }); + else if (Object.isString(wrapper)) wrapper = new Element(wrapper, attributes); + else wrapper = new Element('div', wrapper); + if (element.parentNode) + element.parentNode.replaceChild(wrapper, element); + wrapper.appendChild(element); + return wrapper; + }, + + inspect: function(element) { + element = $(element); + var result = '<' + element.tagName.toLowerCase(); + $H({'id': 'id', 'className': 'class'}).each(function(pair) { + var property = pair.first(), attribute = pair.last(); + var value = (element[property] || '').toString(); + if (value) result += ' ' + attribute + '=' + value.inspect(true); + }); + return result + '>'; + }, + + recursivelyCollect: function(element, property) { + element = $(element); + var elements = []; + while (element = element[property]) + if (element.nodeType == 1) + elements.push(Element.extend(element)); + return elements; + }, + + ancestors: function(element) { + return $(element).recursivelyCollect('parentNode'); + }, + + descendants: function(element) { + return $A($(element).getElementsByTagName('*')).each(Element.extend); + }, + + firstDescendant: function(element) { + element = $(element).firstChild; + while (element && element.nodeType != 1) element = element.nextSibling; + return $(element); + }, + + immediateDescendants: function(element) { + if (!(element = $(element).firstChild)) return []; + while (element && element.nodeType != 1) element = element.nextSibling; + if (element) return [element].concat($(element).nextSiblings()); + return []; + }, + + previousSiblings: function(element) { + return $(element).recursivelyCollect('previousSibling'); + }, + + nextSiblings: function(element) { + return $(element).recursivelyCollect('nextSibling'); + }, + + siblings: function(element) { + element = $(element); + return element.previousSiblings().reverse().concat(element.nextSiblings()); + }, + + match: function(element, selector) { + if (Object.isString(selector)) + selector = new Selector(selector); + return selector.match($(element)); + }, + + up: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(element.parentNode); + var ancestors = element.ancestors(); + return expression ? Selector.findElement(ancestors, expression, index) : + ancestors[index || 0]; + }, + + down: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return element.firstDescendant(); + var descendants = element.descendants(); + return expression ? Selector.findElement(descendants, expression, index) : + descendants[index || 0]; + }, + + previous: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.previousElementSibling(element)); + var previousSiblings = element.previousSiblings(); + return expression ? Selector.findElement(previousSiblings, expression, index) : + previousSiblings[index || 0]; + }, + + next: function(element, expression, index) { + element = $(element); + if (arguments.length == 1) return $(Selector.handlers.nextElementSibling(element)); + var nextSiblings = element.nextSiblings(); + return expression ? Selector.findElement(nextSiblings, expression, index) : + nextSiblings[index || 0]; + }, + + select: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element, args); + }, + + adjacent: function() { + var args = $A(arguments), element = $(args.shift()); + return Selector.findChildElements(element.parentNode, args).without(element); + }, + + identify: function(element) { + element = $(element); + var id = element.readAttribute('id'), self = arguments.callee; + if (id) return id; + do { id = 'anonymous_element_' + self.counter++ } while ($(id)); + element.writeAttribute('id', id); + return id; + }, + + readAttribute: function(element, name) { + element = $(element); + if (Prototype.Browser.IE) { + var t = Element._attributeTranslations.read; + if (t.values[name]) return t.values[name](element, name); + if (t.names[name]) name = t.names[name]; + if (name.include(':')) { + return (!element.attributes || !element.attributes[name]) ? null : + element.attributes[name].value; + } + } + return element.getAttribute(name); + }, + + writeAttribute: function(element, name, value) { + element = $(element); + var attributes = { }, t = Element._attributeTranslations.write; + + if (typeof name == 'object') attributes = name; + else attributes[name] = value === undefined ? true : value; + + for (var attr in attributes) { + var name = t.names[attr] || attr, value = attributes[attr]; + if (t.values[attr]) name = t.values[attr](element, value); + if (value === false || value === null) + element.removeAttribute(name); + else if (value === true) + element.setAttribute(name, name); + else element.setAttribute(name, value); + } + return element; + }, + + getHeight: function(element) { + return $(element).getDimensions().height; + }, + + getWidth: function(element) { + return $(element).getDimensions().width; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + var elementClassName = element.className; + return (elementClassName.length > 0 && (elementClassName == className || + new RegExp("(^|\\s)" + className + "(\\s|$)").test(elementClassName))); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + if (!element.hasClassName(className)) + element.className += (element.className ? ' ' : '') + className; + return element; + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + element.className = element.className.replace( + new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ').strip(); + return element; + }, + + toggleClassName: function(element, className) { + if (!(element = $(element))) return; + return element[element.hasClassName(className) ? + 'removeClassName' : 'addClassName'](className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + var node = element.firstChild; + while (node) { + var nextNode = node.nextSibling; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + element.removeChild(node); + node = nextNode; + } + return element; + }, + + empty: function(element) { + return $(element).innerHTML.blank(); + }, + + descendantOf: function(element, ancestor) { + element = $(element), ancestor = $(ancestor); + + if (element.compareDocumentPosition) + return (element.compareDocumentPosition(ancestor) & 8) === 8; + + if (element.sourceIndex && !Prototype.Browser.Opera) { + var e = element.sourceIndex, a = ancestor.sourceIndex, + nextAncestor = ancestor.nextSibling; + if (!nextAncestor) { + do { ancestor = ancestor.parentNode; } + while (!(nextAncestor = ancestor.nextSibling) && ancestor.parentNode); + } + if (nextAncestor) return (e > a && e < nextAncestor.sourceIndex); + } + + while (element = element.parentNode) + if (element == ancestor) return true; + return false; + }, + + scrollTo: function(element) { + element = $(element); + var pos = element.cumulativeOffset(); + window.scrollTo(pos[0], pos[1]); + return element; + }, + + getStyle: function(element, style) { + element = $(element); + style = style == 'float' ? 'cssFloat' : style.camelize(); + var value = element.style[style]; + if (!value) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css[style] : null; + } + if (style == 'opacity') return value ? parseFloat(value) : 1.0; + return value == 'auto' ? null : value; + }, + + getOpacity: function(element) { + return $(element).getStyle('opacity'); + }, + + setStyle: function(element, styles) { + element = $(element); + var elementStyle = element.style, match; + if (Object.isString(styles)) { + element.style.cssText += ';' + styles; + return styles.include('opacity') ? + element.setOpacity(styles.match(/opacity:\s*(\d?\.?\d*)/)[1]) : element; + } + for (var property in styles) + if (property == 'opacity') element.setOpacity(styles[property]); + else + elementStyle[(property == 'float' || property == 'cssFloat') ? + (elementStyle.styleFloat === undefined ? 'cssFloat' : 'styleFloat') : + property] = styles[property]; + + return element; + }, + + setOpacity: function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + return element; + }, + + getDimensions: function(element) { + element = $(element); + var display = $(element).getStyle('display'); + if (display != 'none' && display != null) // Safari bug + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + var originalDisplay = els.display; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = 'block'; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = originalDisplay; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + return element; + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + return element; + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return element; + element._overflow = Element.getStyle(element, 'overflow') || 'auto'; + if (element._overflow !== 'hidden') + element.style.overflow = 'hidden'; + return element; + }, + + undoClipping: function(element) { + element = $(element); + if (!element._overflow) return element; + element.style.overflow = element._overflow == 'auto' ? '' : element._overflow; + element._overflow = null; + return element; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + if (element.tagName == 'BODY') break; + var p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + absolutize: function(element) { + element = $(element); + if (element.getStyle('position') == 'absolute') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + var offsets = element.positionedOffset(); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.width = width + 'px'; + element.style.height = height + 'px'; + return element; + }, + + relativize: function(element) { + element = $(element); + if (element.getStyle('position') == 'relative') return; + // Position.prepare(); // To be done manually by Scripty when it needs it. + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + return element; + }, + + cumulativeScrollOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return Element._returnOffset(valueL, valueT); + }, + + getOffsetParent: function(element) { + if (element.offsetParent) return $(element.offsetParent); + if (element == document.body) return $(element); + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return $(element); + + return $(document.body); + }, + + viewportOffset: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent == document.body && + Element.getStyle(element, 'position') == 'absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + if (!Prototype.Browser.Opera || element.tagName == 'BODY') { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } + } while (element = element.parentNode); + + return Element._returnOffset(valueL, valueT); + }, + + clonePosition: function(element, source) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || { }); + + // find page position of source + source = $(source); + var p = source.viewportOffset(); + + // find coordinate system to use + element = $(element); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(element, 'position') == 'absolute') { + parent = element.getOffsetParent(); + delta = parent.viewportOffset(); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if (options.setLeft) element.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if (options.setTop) element.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if (options.setWidth) element.style.width = source.offsetWidth + 'px'; + if (options.setHeight) element.style.height = source.offsetHeight + 'px'; + return element; + } +}; + +Element.Methods.identify.counter = 1; + +Object.extend(Element.Methods, { + getElementsBySelector: Element.Methods.select, + childElements: Element.Methods.immediateDescendants +}); + +Element._attributeTranslations = { + write: { + names: { + className: 'class', + htmlFor: 'for' + }, + values: { } + } +}; + + +if (!document.createRange || Prototype.Browser.Opera) { + Element.Methods.insert = function(element, insertions) { + element = $(element); + + if (Object.isString(insertions) || Object.isNumber(insertions) || + Object.isElement(insertions) || (insertions && (insertions.toElement || insertions.toHTML))) + insertions = { bottom: insertions }; + + var t = Element._insertionTranslations, content, position, pos, tagName; + + for (position in insertions) { + content = insertions[position]; + position = position.toLowerCase(); + pos = t[position]; + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + pos.insert(element, content); + continue; + } + + content = Object.toHTML(content); + tagName = ((position == 'before' || position == 'after') + ? element.parentNode : element).tagName.toUpperCase(); + + if (t.tags[tagName]) { + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + if (position == 'top' || position == 'after') fragments.reverse(); + fragments.each(pos.insert.curry(element)); + } + else element.insertAdjacentHTML(pos.adjacency, content.stripScripts()); + + content.evalScripts.bind(content).defer(); + } + + return element; + }; +} + +if (Prototype.Browser.Opera) { + Element.Methods._getStyle = Element.Methods.getStyle; + Element.Methods.getStyle = function(element, style) { + switch(style) { + case 'left': + case 'top': + case 'right': + case 'bottom': + if (Element._getStyle(element, 'position') == 'static') return null; + default: return Element._getStyle(element, style); + } + }; + Element.Methods._readAttribute = Element.Methods.readAttribute; + Element.Methods.readAttribute = function(element, attribute) { + if (attribute == 'title') return element.title; + return Element._readAttribute(element, attribute); + }; +} + +else if (Prototype.Browser.IE) { + $w('positionedOffset getOffsetParent viewportOffset').each(function(method) { + Element.Methods[method] = Element.Methods[method].wrap( + function(proceed, element) { + element = $(element); + var position = element.getStyle('position'); + if (position != 'static') return proceed(element); + element.setStyle({ position: 'relative' }); + var value = proceed(element); + element.setStyle({ position: position }); + return value; + } + ); + }); + + Element.Methods.getStyle = function(element, style) { + element = $(element); + style = (style == 'float' || style == 'cssFloat') ? 'styleFloat' : style.camelize(); + var value = element.style[style]; + if (!value && element.currentStyle) value = element.currentStyle[style]; + + if (style == 'opacity') { + if (value = (element.getStyle('filter') || '').match(/alpha\(opacity=(.*)\)/)) + if (value[1]) return parseFloat(value[1]) / 100; + return 1.0; + } + + if (value == 'auto') { + if ((style == 'width' || style == 'height') && (element.getStyle('display') != 'none')) + return element['offset' + style.capitalize()] + 'px'; + return null; + } + return value; + }; + + Element.Methods.setOpacity = function(element, value) { + function stripAlpha(filter){ + return filter.replace(/alpha\([^\)]*\)/gi,''); + } + element = $(element); + var currentStyle = element.currentStyle; + if ((currentStyle && !currentStyle.hasLayout) || + (!currentStyle && element.style.zoom == 'normal')) + element.style.zoom = 1; + + var filter = element.getStyle('filter'), style = element.style; + if (value == 1 || value === '') { + (filter = stripAlpha(filter)) ? + style.filter = filter : style.removeAttribute('filter'); + return element; + } else if (value < 0.00001) value = 0; + style.filter = stripAlpha(filter) + + 'alpha(opacity=' + (value * 100) + ')'; + return element; + }; + + Element._attributeTranslations = { + read: { + names: { + 'class': 'className', + 'for': 'htmlFor' + }, + values: { + _getAttr: function(element, attribute) { + return element.getAttribute(attribute, 2); + }, + _getAttrNode: function(element, attribute) { + var node = element.getAttributeNode(attribute); + return node ? node.value : ""; + }, + _getEv: function(element, attribute) { + var attribute = element.getAttribute(attribute); + return attribute ? attribute.toString().slice(23, -2) : null; + }, + _flag: function(element, attribute) { + return $(element).hasAttribute(attribute) ? attribute : null; + }, + style: function(element) { + return element.style.cssText.toLowerCase(); + }, + title: function(element) { + return element.title; + } + } + } + }; + + Element._attributeTranslations.write = { + names: Object.clone(Element._attributeTranslations.read.names), + values: { + checked: function(element, value) { + element.checked = !!value; + }, + + style: function(element, value) { + element.style.cssText = value ? value : ''; + } + } + }; + + Element._attributeTranslations.has = {}; + + $w('colSpan rowSpan vAlign dateTime accessKey tabIndex ' + + 'encType maxLength readOnly longDesc').each(function(attr) { + Element._attributeTranslations.write.names[attr.toLowerCase()] = attr; + Element._attributeTranslations.has[attr.toLowerCase()] = attr; + }); + + (function(v) { + Object.extend(v, { + href: v._getAttr, + src: v._getAttr, + type: v._getAttr, + action: v._getAttrNode, + disabled: v._flag, + checked: v._flag, + readonly: v._flag, + multiple: v._flag, + onload: v._getEv, + onunload: v._getEv, + onclick: v._getEv, + ondblclick: v._getEv, + onmousedown: v._getEv, + onmouseup: v._getEv, + onmouseover: v._getEv, + onmousemove: v._getEv, + onmouseout: v._getEv, + onfocus: v._getEv, + onblur: v._getEv, + onkeypress: v._getEv, + onkeydown: v._getEv, + onkeyup: v._getEv, + onsubmit: v._getEv, + onreset: v._getEv, + onselect: v._getEv, + onchange: v._getEv + }); + })(Element._attributeTranslations.read.values); +} + +else if (Prototype.Browser.Gecko && /rv:1\.8\.0/.test(navigator.userAgent)) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1) ? 0.999999 : + (value === '') ? '' : (value < 0.00001) ? 0 : value; + return element; + }; +} + +else if (Prototype.Browser.WebKit) { + Element.Methods.setOpacity = function(element, value) { + element = $(element); + element.style.opacity = (value == 1 || value === '') ? '' : + (value < 0.00001) ? 0 : value; + + if (value == 1) + if(element.tagName == 'IMG' && element.width) { + element.width++; element.width--; + } else try { + var n = document.createTextNode(' '); + element.appendChild(n); + element.removeChild(n); + } catch (e) { } + + return element; + }; + + // Safari returns margins on body which is incorrect if the child is absolutely + // positioned. For performance reasons, redefine Position.cumulativeOffset for + // KHTML/WebKit only. + Element.Methods.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return Element._returnOffset(valueL, valueT); + }; +} + +if (Prototype.Browser.IE || Prototype.Browser.Opera) { + // IE and Opera are missing .innerHTML support for TABLE-related and SELECT elements + Element.Methods.update = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) return element.update().insert(content); + + content = Object.toHTML(content); + var tagName = element.tagName.toUpperCase(); + + if (tagName in Element._insertionTranslations.tags) { + $A(element.childNodes).each(function(node) { element.removeChild(node) }); + Element._getContentFromAnonymousElement(tagName, content.stripScripts()) + .each(function(node) { element.appendChild(node) }); + } + else element.innerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +if (document.createElement('div').outerHTML) { + Element.Methods.replace = function(element, content) { + element = $(element); + + if (content && content.toElement) content = content.toElement(); + if (Object.isElement(content)) { + element.parentNode.replaceChild(content, element); + return element; + } + + content = Object.toHTML(content); + var parent = element.parentNode, tagName = parent.tagName.toUpperCase(); + + if (Element._insertionTranslations.tags[tagName]) { + var nextSibling = element.next(); + var fragments = Element._getContentFromAnonymousElement(tagName, content.stripScripts()); + parent.removeChild(element); + if (nextSibling) + fragments.each(function(node) { parent.insertBefore(node, nextSibling) }); + else + fragments.each(function(node) { parent.appendChild(node) }); + } + else element.outerHTML = content.stripScripts(); + + content.evalScripts.bind(content).defer(); + return element; + }; +} + +Element._returnOffset = function(l, t) { + var result = [l, t]; + result.left = l; + result.top = t; + return result; +}; + +Element._getContentFromAnonymousElement = function(tagName, html) { + var div = new Element('div'), t = Element._insertionTranslations.tags[tagName]; + div.innerHTML = t[0] + html + t[1]; + t[2].times(function() { div = div.firstChild }); + return $A(div.childNodes); +}; + +Element._insertionTranslations = { + before: { + adjacency: 'beforeBegin', + insert: function(element, node) { + element.parentNode.insertBefore(node, element); + }, + initializeRange: function(element, range) { + range.setStartBefore(element); + } + }, + top: { + adjacency: 'afterBegin', + insert: function(element, node) { + element.insertBefore(node, element.firstChild); + }, + initializeRange: function(element, range) { + range.selectNodeContents(element); + range.collapse(true); + } + }, + bottom: { + adjacency: 'beforeEnd', + insert: function(element, node) { + element.appendChild(node); + } + }, + after: { + adjacency: 'afterEnd', + insert: function(element, node) { + element.parentNode.insertBefore(node, element.nextSibling); + }, + initializeRange: function(element, range) { + range.setStartAfter(element); + } + }, + tags: { + TABLE: ['', '
', 1], + TBODY: ['', '
', 2], + TR: ['', '
', 3], + TD: ['
', '
', 4], + SELECT: ['', 1] + } +}; + +(function() { + this.bottom.initializeRange = this.top.initializeRange; + Object.extend(this.tags, { + THEAD: this.tags.TBODY, + TFOOT: this.tags.TBODY, + TH: this.tags.TD + }); +}).call(Element._insertionTranslations); + +Element.Methods.Simulated = { + hasAttribute: function(element, attribute) { + attribute = Element._attributeTranslations.has[attribute] || attribute; + var node = $(element).getAttributeNode(attribute); + return node && node.specified; + } +}; + +Element.Methods.ByTag = { }; + +Object.extend(Element, Element.Methods); + +if (!Prototype.BrowserFeatures.ElementExtensions && + document.createElement('div').__proto__) { + window.HTMLElement = { }; + window.HTMLElement.prototype = document.createElement('div').__proto__; + Prototype.BrowserFeatures.ElementExtensions = true; +} + +Element.extend = (function() { + if (Prototype.BrowserFeatures.SpecificElementExtensions) + return Prototype.K; + + var Methods = { }, ByTag = Element.Methods.ByTag; + + var extend = Object.extend(function(element) { + if (!element || element._extendedByPrototype || + element.nodeType != 1 || element == window) return element; + + var methods = Object.clone(Methods), + tagName = element.tagName, property, value; + + // extend methods for specific tags + if (ByTag[tagName]) Object.extend(methods, ByTag[tagName]); + + for (property in methods) { + value = methods[property]; + if (Object.isFunction(value) && !(property in element)) + element[property] = value.methodize(); + } + + element._extendedByPrototype = Prototype.emptyFunction; + return element; + + }, { + refresh: function() { + // extend methods for all tags (Safari doesn't need this) + if (!Prototype.BrowserFeatures.ElementExtensions) { + Object.extend(Methods, Element.Methods); + Object.extend(Methods, Element.Methods.Simulated); + } + } + }); + + extend.refresh(); + return extend; +})(); + +Element.hasAttribute = function(element, attribute) { + if (element.hasAttribute) return element.hasAttribute(attribute); + return Element.Methods.Simulated.hasAttribute(element, attribute); +}; + +Element.addMethods = function(methods) { + var F = Prototype.BrowserFeatures, T = Element.Methods.ByTag; + + if (!methods) { + Object.extend(Form, Form.Methods); + Object.extend(Form.Element, Form.Element.Methods); + Object.extend(Element.Methods.ByTag, { + "FORM": Object.clone(Form.Methods), + "INPUT": Object.clone(Form.Element.Methods), + "SELECT": Object.clone(Form.Element.Methods), + "TEXTAREA": Object.clone(Form.Element.Methods) + }); + } + + if (arguments.length == 2) { + var tagName = methods; + methods = arguments[1]; + } + + if (!tagName) Object.extend(Element.Methods, methods || { }); + else { + if (Object.isArray(tagName)) tagName.each(extend); + else extend(tagName); + } + + function extend(tagName) { + tagName = tagName.toUpperCase(); + if (!Element.Methods.ByTag[tagName]) + Element.Methods.ByTag[tagName] = { }; + Object.extend(Element.Methods.ByTag[tagName], methods); + } + + function copy(methods, destination, onlyIfAbsent) { + onlyIfAbsent = onlyIfAbsent || false; + for (var property in methods) { + var value = methods[property]; + if (!Object.isFunction(value)) continue; + if (!onlyIfAbsent || !(property in destination)) + destination[property] = value.methodize(); + } + } + + function findDOMClass(tagName) { + var klass; + var trans = { + "OPTGROUP": "OptGroup", "TEXTAREA": "TextArea", "P": "Paragraph", + "FIELDSET": "FieldSet", "UL": "UList", "OL": "OList", "DL": "DList", + "DIR": "Directory", "H1": "Heading", "H2": "Heading", "H3": "Heading", + "H4": "Heading", "H5": "Heading", "H6": "Heading", "Q": "Quote", + "INS": "Mod", "DEL": "Mod", "A": "Anchor", "IMG": "Image", "CAPTION": + "TableCaption", "COL": "TableCol", "COLGROUP": "TableCol", "THEAD": + "TableSection", "TFOOT": "TableSection", "TBODY": "TableSection", "TR": + "TableRow", "TH": "TableCell", "TD": "TableCell", "FRAMESET": + "FrameSet", "IFRAME": "IFrame" + }; + if (trans[tagName]) klass = 'HTML' + trans[tagName] + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName + 'Element'; + if (window[klass]) return window[klass]; + klass = 'HTML' + tagName.capitalize() + 'Element'; + if (window[klass]) return window[klass]; + + window[klass] = { }; + window[klass].prototype = document.createElement(tagName).__proto__; + return window[klass]; + } + + if (F.ElementExtensions) { + copy(Element.Methods, HTMLElement.prototype); + copy(Element.Methods.Simulated, HTMLElement.prototype, true); + } + + if (F.SpecificElementExtensions) { + for (var tag in Element.Methods.ByTag) { + var klass = findDOMClass(tag); + if (Object.isUndefined(klass)) continue; + copy(T[tag], klass.prototype); + } + } + + Object.extend(Element, Element.Methods); + delete Element.ByTag; + + if (Element.extend.refresh) Element.extend.refresh(); + Element.cache = { }; +}; + +document.viewport = { + getDimensions: function() { + var dimensions = { }; + $w('width height').each(function(d) { + var D = d.capitalize(); + dimensions[d] = self['inner' + D] || + (document.documentElement['client' + D] || document.body['client' + D]); + }); + return dimensions; + }, + + getWidth: function() { + return this.getDimensions().width; + }, + + getHeight: function() { + return this.getDimensions().height; + }, + + getScrollOffsets: function() { + return Element._returnOffset( + window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft, + window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop); + } +}; +/* Portions of the Selector class are derived from Jack Slocum’s DomQuery, + * part of YUI-Ext version 0.40, distributed under the terms of an MIT-style + * license. Please see http://www.yui-ext.com/ for more information. */ + +var Selector = Class.create({ + initialize: function(expression) { + this.expression = expression.strip(); + this.compileMatcher(); + }, + + compileMatcher: function() { + // Selectors with namespaced attributes can't use the XPath version + if (Prototype.BrowserFeatures.XPath && !(/(\[[\w-]*?:|:checked)/).test(this.expression)) + return this.compileXPathMatcher(); + + var e = this.expression, ps = Selector.patterns, h = Selector.handlers, + c = Selector.criteria, le, p, m; + + if (Selector._cache[e]) { + this.matcher = Selector._cache[e]; + return; + } + + this.matcher = ["this.matcher = function(root) {", + "var r = root, h = Selector.handlers, c = false, n;"]; + + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + this.matcher.push(Object.isFunction(c[i]) ? c[i](m) : + new Template(c[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.matcher.push("return h.unique(n);\n}"); + eval(this.matcher.join('\n')); + Selector._cache[this.expression] = this.matcher; + }, + + compileXPathMatcher: function() { + var e = this.expression, ps = Selector.patterns, + x = Selector.xpath, le, m; + + if (Selector._cache[e]) { + this.xpath = Selector._cache[e]; return; + } + + this.matcher = ['.//*']; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + if (m = e.match(ps[i])) { + this.matcher.push(Object.isFunction(x[i]) ? x[i](m) : + new Template(x[i]).evaluate(m)); + e = e.replace(m[0], ''); + break; + } + } + } + + this.xpath = this.matcher.join(''); + Selector._cache[this.expression] = this.xpath; + }, + + findElements: function(root) { + root = root || document; + if (this.xpath) return document._getElementsByXPath(this.xpath, root); + return this.matcher(root); + }, + + match: function(element) { + this.tokens = []; + + var e = this.expression, ps = Selector.patterns, as = Selector.assertions; + var le, p, m; + + while (e && le !== e && (/\S/).test(e)) { + le = e; + for (var i in ps) { + p = ps[i]; + if (m = e.match(p)) { + // use the Selector.assertions methods unless the selector + // is too complex. + if (as[i]) { + this.tokens.push([i, Object.clone(m)]); + e = e.replace(m[0], ''); + } else { + // reluctantly do a document-wide search + // and look for a match in the array + return this.findElements(document).include(element); + } + } + } + } + + var match = true, name, matches; + for (var i = 0, token; token = this.tokens[i]; i++) { + name = token[0], matches = token[1]; + if (!Selector.assertions[name](element, matches)) { + match = false; break; + } + } + + return match; + }, + + toString: function() { + return this.expression; + }, + + inspect: function() { + return "#"; + } +}); + +Object.extend(Selector, { + _cache: { }, + + xpath: { + descendant: "//*", + child: "/*", + adjacent: "/following-sibling::*[1]", + laterSibling: '/following-sibling::*', + tagName: function(m) { + if (m[1] == '*') return ''; + return "[local-name()='" + m[1].toLowerCase() + + "' or local-name()='" + m[1].toUpperCase() + "']"; + }, + className: "[contains(concat(' ', @class, ' '), ' #{1} ')]", + id: "[@id='#{1}']", + attrPresence: "[@#{1}]", + attr: function(m) { + m[3] = m[5] || m[6]; + return new Template(Selector.xpath.operators[m[2]]).evaluate(m); + }, + pseudo: function(m) { + var h = Selector.xpath.pseudos[m[1]]; + if (!h) return ''; + if (Object.isFunction(h)) return h(m); + return new Template(Selector.xpath.pseudos[m[1]]).evaluate(m); + }, + operators: { + '=': "[@#{1}='#{3}']", + '!=': "[@#{1}!='#{3}']", + '^=': "[starts-with(@#{1}, '#{3}')]", + '$=': "[substring(@#{1}, (string-length(@#{1}) - string-length('#{3}') + 1))='#{3}']", + '*=': "[contains(@#{1}, '#{3}')]", + '~=': "[contains(concat(' ', @#{1}, ' '), ' #{3} ')]", + '|=': "[contains(concat('-', @#{1}, '-'), '-#{3}-')]" + }, + pseudos: { + 'first-child': '[not(preceding-sibling::*)]', + 'last-child': '[not(following-sibling::*)]', + 'only-child': '[not(preceding-sibling::* or following-sibling::*)]', + 'empty': "[count(*) = 0 and (count(text()) = 0 or translate(text(), ' \t\r\n', '') = '')]", + 'checked': "[@checked]", + 'disabled': "[@disabled]", + 'enabled': "[not(@disabled)]", + 'not': function(m) { + var e = m[6], p = Selector.patterns, + x = Selector.xpath, le, m, v; + + var exclusion = []; + while (e && le != e && (/\S/).test(e)) { + le = e; + for (var i in p) { + if (m = e.match(p[i])) { + v = Object.isFunction(x[i]) ? x[i](m) : new Template(x[i]).evaluate(m); + exclusion.push("(" + v.substring(1, v.length - 1) + ")"); + e = e.replace(m[0], ''); + break; + } + } + } + return "[not(" + exclusion.join(" and ") + ")]"; + }, + 'nth-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./preceding-sibling::*) + 1) ", m); + }, + 'nth-last-child': function(m) { + return Selector.xpath.pseudos.nth("(count(./following-sibling::*) + 1) ", m); + }, + 'nth-of-type': function(m) { + return Selector.xpath.pseudos.nth("position() ", m); + }, + 'nth-last-of-type': function(m) { + return Selector.xpath.pseudos.nth("(last() + 1 - position()) ", m); + }, + 'first-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-of-type'](m); + }, + 'last-of-type': function(m) { + m[6] = "1"; return Selector.xpath.pseudos['nth-last-of-type'](m); + }, + 'only-of-type': function(m) { + var p = Selector.xpath.pseudos; return p['first-of-type'](m) + p['last-of-type'](m); + }, + nth: function(fragment, m) { + var mm, formula = m[6], predicate; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + if (mm = formula.match(/^(\d+)$/)) // digit only + return '[' + fragment + "= " + mm[1] + ']'; + if (mm = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (mm[1] == "-") mm[1] = -1; + var a = mm[1] ? Number(mm[1]) : 1; + var b = mm[2] ? Number(mm[2]) : 0; + predicate = "[((#{fragment} - #{b}) mod #{a} = 0) and " + + "((#{fragment} - #{b}) div #{a} >= 0)]"; + return new Template(predicate).evaluate({ + fragment: fragment, a: a, b: b }); + } + } + } + }, + + criteria: { + tagName: 'n = h.tagName(n, r, "#{1}", c); c = false;', + className: 'n = h.className(n, r, "#{1}", c); c = false;', + id: 'n = h.id(n, r, "#{1}", c); c = false;', + attrPresence: 'n = h.attrPresence(n, r, "#{1}"); c = false;', + attr: function(m) { + m[3] = (m[5] || m[6]); + return new Template('n = h.attr(n, r, "#{1}", "#{3}", "#{2}"); c = false;').evaluate(m); + }, + pseudo: function(m) { + if (m[6]) m[6] = m[6].replace(/"/g, '\\"'); + return new Template('n = h.pseudo(n, "#{1}", "#{6}", r, c); c = false;').evaluate(m); + }, + descendant: 'c = "descendant";', + child: 'c = "child";', + adjacent: 'c = "adjacent";', + laterSibling: 'c = "laterSibling";' + }, + + patterns: { + // combinators must be listed first + // (and descendant needs to be last combinator) + laterSibling: /^\s*~\s*/, + child: /^\s*>\s*/, + adjacent: /^\s*\+\s*/, + descendant: /^\s/, + + // selectors follow + tagName: /^\s*(\*|[\w\-]+)(\b|$)?/, + id: /^#([\w\-\*]+)(\b|$)/, + className: /^\.([\w\-\*]+)(\b|$)/, + pseudo: /^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|dis)abled|not)(\((.*?)\))?(\b|$|(?=\s)|(?=:))/, + attrPresence: /^\[([\w]+)\]/, + attr: /\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^\]]*?)))?\]/ + }, + + // for Selector.match and Element#match + assertions: { + tagName: function(element, matches) { + return matches[1].toUpperCase() == element.tagName.toUpperCase(); + }, + + className: function(element, matches) { + return Element.hasClassName(element, matches[1]); + }, + + id: function(element, matches) { + return element.id === matches[1]; + }, + + attrPresence: function(element, matches) { + return Element.hasAttribute(element, matches[1]); + }, + + attr: function(element, matches) { + var nodeValue = Element.readAttribute(element, matches[1]); + return Selector.operators[matches[2]](nodeValue, matches[3]); + } + }, + + handlers: { + // UTILITY FUNCTIONS + // joins two collections + concat: function(a, b) { + for (var i = 0, node; node = b[i]; i++) + a.push(node); + return a; + }, + + // marks an array of nodes for counting + mark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = true; + return nodes; + }, + + unmark: function(nodes) { + for (var i = 0, node; node = nodes[i]; i++) + node._counted = undefined; + return nodes; + }, + + // mark each child node with its position (for nth calls) + // "ofType" flag indicates whether we're indexing for nth-of-type + // rather than nth-child + index: function(parentNode, reverse, ofType) { + parentNode._counted = true; + if (reverse) { + for (var nodes = parentNode.childNodes, i = nodes.length - 1, j = 1; i >= 0; i--) { + var node = nodes[i]; + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + } else { + for (var i = 0, j = 1, nodes = parentNode.childNodes; node = nodes[i]; i++) + if (node.nodeType == 1 && (!ofType || node._counted)) node.nodeIndex = j++; + } + }, + + // filters out duplicates and extends all nodes + unique: function(nodes) { + if (nodes.length == 0) return nodes; + var results = [], n; + for (var i = 0, l = nodes.length; i < l; i++) + if (!(n = nodes[i])._counted) { + n._counted = true; + results.push(Element.extend(n)); + } + return Selector.handlers.unmark(results); + }, + + // COMBINATOR FUNCTIONS + descendant: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName('*')); + return results; + }, + + child: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) { + for (var j = 0, children = [], child; child = node.childNodes[j]; j++) + if (child.nodeType == 1 && child.tagName != '!') results.push(child); + } + return results; + }, + + adjacent: function(nodes) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + var next = this.nextElementSibling(node); + if (next) results.push(next); + } + return results; + }, + + laterSibling: function(nodes) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + h.concat(results, Element.nextSiblings(node)); + return results; + }, + + nextElementSibling: function(node) { + while (node = node.nextSibling) + if (node.nodeType == 1) return node; + return null; + }, + + previousElementSibling: function(node) { + while (node = node.previousSibling) + if (node.nodeType == 1) return node; + return null; + }, + + // TOKEN FUNCTIONS + tagName: function(nodes, root, tagName, combinator) { + tagName = tagName.toUpperCase(); + var results = [], h = Selector.handlers; + if (nodes) { + if (combinator) { + // fastlane for ordinary descendant combinators + if (combinator == "descendant") { + for (var i = 0, node; node = nodes[i]; i++) + h.concat(results, node.getElementsByTagName(tagName)); + return results; + } else nodes = this[combinator](nodes); + if (tagName == "*") return nodes; + } + for (var i = 0, node; node = nodes[i]; i++) + if (node.tagName.toUpperCase() == tagName) results.push(node); + return results; + } else return root.getElementsByTagName(tagName); + }, + + id: function(nodes, root, id, combinator) { + var targetNode = $(id), h = Selector.handlers; + if (!targetNode) return []; + if (!nodes && root == document) return [targetNode]; + if (nodes) { + if (combinator) { + if (combinator == 'child') { + for (var i = 0, node; node = nodes[i]; i++) + if (targetNode.parentNode == node) return [targetNode]; + } else if (combinator == 'descendant') { + for (var i = 0, node; node = nodes[i]; i++) + if (Element.descendantOf(targetNode, node)) return [targetNode]; + } else if (combinator == 'adjacent') { + for (var i = 0, node; node = nodes[i]; i++) + if (Selector.handlers.previousElementSibling(targetNode) == node) + return [targetNode]; + } else nodes = h[combinator](nodes); + } + for (var i = 0, node; node = nodes[i]; i++) + if (node == targetNode) return [targetNode]; + return []; + } + return (targetNode && Element.descendantOf(targetNode, root)) ? [targetNode] : []; + }, + + className: function(nodes, root, className, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + return Selector.handlers.byClassName(nodes, root, className); + }, + + byClassName: function(nodes, root, className) { + if (!nodes) nodes = Selector.handlers.descendant([root]); + var needle = ' ' + className + ' '; + for (var i = 0, results = [], node, nodeClassName; node = nodes[i]; i++) { + nodeClassName = node.className; + if (nodeClassName.length == 0) continue; + if (nodeClassName == className || (' ' + nodeClassName + ' ').include(needle)) + results.push(node); + } + return results; + }, + + attrPresence: function(nodes, root, attr) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var results = []; + for (var i = 0, node; node = nodes[i]; i++) + if (Element.hasAttribute(node, attr)) results.push(node); + return results; + }, + + attr: function(nodes, root, attr, value, operator) { + if (!nodes) nodes = root.getElementsByTagName("*"); + var handler = Selector.operators[operator], results = []; + for (var i = 0, node; node = nodes[i]; i++) { + var nodeValue = Element.readAttribute(node, attr); + if (nodeValue === null) continue; + if (handler(nodeValue, value)) results.push(node); + } + return results; + }, + + pseudo: function(nodes, name, value, root, combinator) { + if (nodes && combinator) nodes = this[combinator](nodes); + if (!nodes) nodes = root.getElementsByTagName("*"); + return Selector.pseudos[name](nodes, value, root); + } + }, + + pseudos: { + 'first-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.previousElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'last-child': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + if (Selector.handlers.nextElementSibling(node)) continue; + results.push(node); + } + return results; + }, + 'only-child': function(nodes, value, root) { + var h = Selector.handlers; + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!h.previousElementSibling(node) && !h.nextElementSibling(node)) + results.push(node); + return results; + }, + 'nth-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root); + }, + 'nth-last-child': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true); + }, + 'nth-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, false, true); + }, + 'nth-last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, formula, root, true, true); + }, + 'first-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, false, true); + }, + 'last-of-type': function(nodes, formula, root) { + return Selector.pseudos.nth(nodes, "1", root, true, true); + }, + 'only-of-type': function(nodes, formula, root) { + var p = Selector.pseudos; + return p['last-of-type'](p['first-of-type'](nodes, formula, root), formula, root); + }, + + // handles the an+b logic + getIndices: function(a, b, total) { + if (a == 0) return b > 0 ? [b] : []; + return $R(1, total).inject([], function(memo, i) { + if (0 == (i - b) % a && (i - b) / a >= 0) memo.push(i); + return memo; + }); + }, + + // handles nth(-last)-child, nth(-last)-of-type, and (first|last)-of-type + nth: function(nodes, formula, root, reverse, ofType) { + if (nodes.length == 0) return []; + if (formula == 'even') formula = '2n+0'; + if (formula == 'odd') formula = '2n+1'; + var h = Selector.handlers, results = [], indexed = [], m; + h.mark(nodes); + for (var i = 0, node; node = nodes[i]; i++) { + if (!node.parentNode._counted) { + h.index(node.parentNode, reverse, ofType); + indexed.push(node.parentNode); + } + } + if (formula.match(/^\d+$/)) { // just a number + formula = Number(formula); + for (var i = 0, node; node = nodes[i]; i++) + if (node.nodeIndex == formula) results.push(node); + } else if (m = formula.match(/^(-?\d*)?n(([+-])(\d+))?/)) { // an+b + if (m[1] == "-") m[1] = -1; + var a = m[1] ? Number(m[1]) : 1; + var b = m[2] ? Number(m[2]) : 0; + var indices = Selector.pseudos.getIndices(a, b, nodes.length); + for (var i = 0, node, l = indices.length; node = nodes[i]; i++) { + for (var j = 0; j < l; j++) + if (node.nodeIndex == indices[j]) results.push(node); + } + } + h.unmark(nodes); + h.unmark(indexed); + return results; + }, + + 'empty': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) { + // IE treats comments as element nodes + if (node.tagName == '!' || (node.firstChild && !node.innerHTML.match(/^\s*$/))) continue; + results.push(node); + } + return results; + }, + + 'not': function(nodes, selector, root) { + var h = Selector.handlers, selectorType, m; + var exclusions = new Selector(selector).findElements(root); + h.mark(exclusions); + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node._counted) results.push(node); + h.unmark(exclusions); + return results; + }, + + 'enabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (!node.disabled) results.push(node); + return results; + }, + + 'disabled': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.disabled) results.push(node); + return results; + }, + + 'checked': function(nodes, value, root) { + for (var i = 0, results = [], node; node = nodes[i]; i++) + if (node.checked) results.push(node); + return results; + } + }, + + operators: { + '=': function(nv, v) { return nv == v; }, + '!=': function(nv, v) { return nv != v; }, + '^=': function(nv, v) { return nv.startsWith(v); }, + '$=': function(nv, v) { return nv.endsWith(v); }, + '*=': function(nv, v) { return nv.include(v); }, + '~=': function(nv, v) { return (' ' + nv + ' ').include(' ' + v + ' '); }, + '|=': function(nv, v) { return ('-' + nv.toUpperCase() + '-').include('-' + v.toUpperCase() + '-'); } + }, + + matchElements: function(elements, expression) { + var matches = new Selector(expression).findElements(), h = Selector.handlers; + h.mark(matches); + for (var i = 0, results = [], element; element = elements[i]; i++) + if (element._counted) results.push(element); + h.unmark(matches); + return results; + }, + + findElement: function(elements, expression, index) { + if (Object.isNumber(expression)) { + index = expression; expression = false; + } + return Selector.matchElements(elements, expression || '*')[index || 0]; + }, + + findChildElements: function(element, expressions) { + var exprs = expressions.join(','), expressions = []; + exprs.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) { + expressions.push(m[1].strip()); + }); + var results = [], h = Selector.handlers; + for (var i = 0, l = expressions.length, selector; i < l; i++) { + selector = new Selector(expressions[i].strip()); + h.concat(results, selector.findElements(element)); + } + return (l > 1) ? h.unique(results) : results; + } +}); + +function $$() { + return Selector.findChildElements(document, $A(arguments)); +} +var Form = { + reset: function(form) { + $(form).reset(); + return form; + }, + + serializeElements: function(elements, options) { + if (typeof options != 'object') options = { hash: !!options }; + else if (options.hash === undefined) options.hash = true; + var key, value, submitted = false, submit = options.submit; + + var data = elements.inject({ }, function(result, element) { + if (!element.disabled && element.name) { + key = element.name; value = $(element).getValue(); + if (value != null && (element.type != 'submit' || (!submitted && + submit !== false && (!submit || key == submit) && (submitted = true)))) { + if (key in result) { + // a key is already present; construct an array of values + if (!Object.isArray(result[key])) result[key] = [result[key]]; + result[key].push(value); + } + else result[key] = value; + } + } + return result; + }); + + return options.hash ? data : Object.toQueryString(data); + } +}; + +Form.Methods = { + serialize: function(form, options) { + return Form.serializeElements(Form.getElements(form), options); + }, + + getElements: function(form) { + return $A($(form).getElementsByTagName('*')).inject([], + function(elements, child) { + if (Form.Element.Serializers[child.tagName.toLowerCase()]) + elements.push(Element.extend(child)); + return elements; + } + ); + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) return $A(inputs).map(Element.extend); + + for (var i = 0, matchingInputs = [], length = inputs.length; i < length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || (name && input.name != name)) + continue; + matchingInputs.push(Element.extend(input)); + } + + return matchingInputs; + }, + + disable: function(form) { + form = $(form); + Form.getElements(form).invoke('disable'); + return form; + }, + + enable: function(form) { + form = $(form); + Form.getElements(form).invoke('enable'); + return form; + }, + + findFirstElement: function(form) { + var elements = $(form).getElements().findAll(function(element) { + return 'hidden' != element.type && !element.disabled; + }); + var firstByIndex = elements.findAll(function(element) { + return element.hasAttribute('tabIndex') && element.tabIndex >= 0; + }).sortBy(function(element) { return element.tabIndex }).first(); + + return firstByIndex ? firstByIndex : elements.find(function(element) { + return ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + form = $(form); + form.findFirstElement().activate(); + return form; + }, + + request: function(form, options) { + form = $(form), options = Object.clone(options || { }); + + var params = options.parameters, action = form.readAttribute('action') || ''; + if (action.blank()) action = window.location.href; + options.parameters = form.serialize(true); + + if (params) { + if (Object.isString(params)) params = params.toQueryParams(); + Object.extend(options.parameters, params); + } + + if (form.hasAttribute('method') && !options.method) + options.method = form.method; + + return new Ajax.Request(action, options); + } +}; + +/*--------------------------------------------------------------------------*/ + +Form.Element = { + focus: function(element) { + $(element).focus(); + return element; + }, + + select: function(element) { + $(element).select(); + return element; + } +}; + +Form.Element.Methods = { + serialize: function(element) { + element = $(element); + if (!element.disabled && element.name) { + var value = element.getValue(); + if (value != undefined) { + var pair = { }; + pair[element.name] = value; + return Object.toQueryString(pair); + } + } + return ''; + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + return Form.Element.Serializers[method](element); + }, + + setValue: function(element, value) { + element = $(element); + var method = element.tagName.toLowerCase(); + Form.Element.Serializers[method](element, value); + return element; + }, + + clear: function(element) { + $(element).value = ''; + return element; + }, + + present: function(element) { + return $(element).value != ''; + }, + + activate: function(element) { + element = $(element); + try { + element.focus(); + if (element.select && (element.tagName.toLowerCase() != 'input' || + !['button', 'reset', 'submit'].include(element.type))) + element.select(); + } catch (e) { } + return element; + }, + + disable: function(element) { + element = $(element); + element.blur(); + element.disabled = true; + return element; + }, + + enable: function(element) { + element = $(element); + element.disabled = false; + return element; + } +}; + +/*--------------------------------------------------------------------------*/ + +var Field = Form.Element; +var $F = Form.Element.Methods.getValue; + +/*--------------------------------------------------------------------------*/ + +Form.Element.Serializers = { + input: function(element, value) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element, value); + default: + return Form.Element.Serializers.textarea(element, value); + } + }, + + inputSelector: function(element, value) { + if (value === undefined) return element.checked ? element.value : null; + else element.checked = !!value; + }, + + textarea: function(element, value) { + if (value === undefined) return element.value; + else element.value = value; + }, + + select: function(element, index) { + if (index === undefined) + return this[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + else { + var opt, value, single = !Object.isArray(index); + for (var i = 0, length = element.length; i < length; i++) { + opt = element.options[i]; + value = this.optionValue(opt); + if (single) { + if (value == index) { + opt.selected = true; + return; + } + } + else opt.selected = index.include(value); + } + } + }, + + selectOne: function(element) { + var index = element.selectedIndex; + return index >= 0 ? this.optionValue(element.options[index]) : null; + }, + + selectMany: function(element) { + var values, length = element.length; + if (!length) return null; + + for (var i = 0, values = []; i < length; i++) { + var opt = element.options[i]; + if (opt.selected) values.push(this.optionValue(opt)); + } + return values; + }, + + optionValue: function(opt) { + // extend element because hasAttribute may not be native + return Element.extend(opt).hasAttribute('value') ? opt.value : opt.text; + } +}; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = Class.create(PeriodicalExecuter, { + initialize: function($super, element, frequency, callback) { + $super(callback, frequency); + this.element = $(element); + this.lastValue = this.getValue(); + }, + + execute: function() { + var value = this.getValue(); + if (Object.isString(this.lastValue) && Object.isString(value) ? + this.lastValue != value : String(this.lastValue) != String(value)) { + this.callback(this.element, value); + this.lastValue = value; + } + } +}); + +Form.Element.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(Abstract.TimedObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = Class.create({ + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + Form.getElements(this.element).each(this.registerCallback, this); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + default: + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +}); + +Form.Element.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(Abstract.EventObserver, { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) var Event = { }; + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + KEY_HOME: 36, + KEY_END: 35, + KEY_PAGEUP: 33, + KEY_PAGEDOWN: 34, + KEY_INSERT: 45, + + cache: { }, + + relatedTarget: function(event) { + var element; + switch(event.type) { + case 'mouseover': element = event.fromElement; break; + case 'mouseout': element = event.toElement; break; + default: return null; + } + return Element.extend(element); + } +}); + +Event.Methods = (function() { + var isButton; + + if (Prototype.Browser.IE) { + var buttonMap = { 0: 1, 1: 4, 2: 2 }; + isButton = function(event, code) { + return event.button == buttonMap[code]; + }; + + } else if (Prototype.Browser.WebKit) { + isButton = function(event, code) { + switch (code) { + case 0: return event.which == 1 && !event.metaKey; + case 1: return event.which == 1 && event.metaKey; + default: return false; + } + }; + + } else { + isButton = function(event, code) { + return event.which ? (event.which === code + 1) : (event.button === code); + }; + } + + return { + isLeftClick: function(event) { return isButton(event, 0) }, + isMiddleClick: function(event) { return isButton(event, 1) }, + isRightClick: function(event) { return isButton(event, 2) }, + + element: function(event) { + var node = Event.extend(event).target; + return Element.extend(node.nodeType == Node.TEXT_NODE ? node.parentNode : node); + }, + + findElement: function(event, expression) { + var element = Event.element(event); + return element.match(expression) ? element : element.up(expression); + }, + + pointer: function(event) { + return { + x: event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)), + y: event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)) + }; + }, + + pointerX: function(event) { return Event.pointer(event).x }, + pointerY: function(event) { return Event.pointer(event).y }, + + stop: function(event) { + Event.extend(event); + event.preventDefault(); + event.stopPropagation(); + event.stopped = true; + } + }; +})(); + +Event.extend = (function() { + var methods = Object.keys(Event.Methods).inject({ }, function(m, name) { + m[name] = Event.Methods[name].methodize(); + return m; + }); + + if (Prototype.Browser.IE) { + Object.extend(methods, { + stopPropagation: function() { this.cancelBubble = true }, + preventDefault: function() { this.returnValue = false }, + inspect: function() { return "[object Event]" } + }); + + return function(event) { + if (!event) return false; + if (event._extendedByPrototype) return event; + + event._extendedByPrototype = Prototype.emptyFunction; + var pointer = Event.pointer(event); + Object.extend(event, { + target: event.srcElement, + relatedTarget: Event.relatedTarget(event), + pageX: pointer.x, + pageY: pointer.y + }); + return Object.extend(event, methods); + }; + + } else { + Event.prototype = Event.prototype || document.createEvent("HTMLEvents").__proto__; + Object.extend(Event.prototype, methods); + return Prototype.K; + } +})(); + +Object.extend(Event, (function() { + var cache = Event.cache; + + function getEventID(element) { + if (element._eventID) return element._eventID; + arguments.callee.id = arguments.callee.id || 1; + return element._eventID = ++arguments.callee.id; + } + + function getDOMEventName(eventName) { + if (eventName && eventName.include(':')) return "dataavailable"; + return eventName; + } + + function getCacheForID(id) { + return cache[id] = cache[id] || { }; + } + + function getWrappersForEventName(id, eventName) { + var c = getCacheForID(id); + return c[eventName] = c[eventName] || []; + } + + function createWrapper(element, eventName, handler) { + var id = getEventID(element); + var c = getWrappersForEventName(id, eventName); + if (c.pluck("handler").include(handler)) return false; + + var wrapper = function(event) { + if (!Event || !Event.extend || + (event.eventName && event.eventName != eventName)) + return false; + + Event.extend(event); + handler.call(element, event) + }; + + wrapper.handler = handler; + c.push(wrapper); + return wrapper; + } + + function findWrapper(id, eventName, handler) { + var c = getWrappersForEventName(id, eventName); + return c.find(function(wrapper) { return wrapper.handler == handler }); + } + + function destroyWrapper(id, eventName, handler) { + var c = getCacheForID(id); + if (!c[eventName]) return false; + c[eventName] = c[eventName].without(findWrapper(id, eventName, handler)); + } + + function destroyCache() { + for (var id in cache) + for (var eventName in cache[id]) + cache[id][eventName] = null; + } + + if (window.attachEvent) { + window.attachEvent("onunload", destroyCache); + } + + return { + observe: function(element, eventName, handler) { + element = $(element); + var name = getDOMEventName(eventName); + + var wrapper = createWrapper(element, eventName, handler); + if (!wrapper) return element; + + if (element.addEventListener) { + element.addEventListener(name, wrapper, false); + } else { + element.attachEvent("on" + name, wrapper); + } + + return element; + }, + + stopObserving: function(element, eventName, handler) { + element = $(element); + var id = getEventID(element), name = getDOMEventName(eventName); + + if (!handler && eventName) { + getWrappersForEventName(id, eventName).each(function(wrapper) { + element.stopObserving(eventName, wrapper.handler); + }); + return element; + + } else if (!eventName) { + Object.keys(getCacheForID(id)).each(function(eventName) { + element.stopObserving(eventName); + }); + return element; + } + + var wrapper = findWrapper(id, eventName, handler); + if (!wrapper) return element; + + if (element.removeEventListener) { + element.removeEventListener(name, wrapper, false); + } else { + element.detachEvent("on" + name, wrapper); + } + + destroyWrapper(id, eventName, handler); + + return element; + }, + + fire: function(element, eventName, memo) { + element = $(element); + if (element == document && document.createEvent && !element.dispatchEvent) + element = document.documentElement; + + if (document.createEvent) { + var event = document.createEvent("HTMLEvents"); + event.initEvent("dataavailable", true, true); + } else { + var event = document.createEventObject(); + event.eventType = "ondataavailable"; + } + + event.eventName = eventName; + event.memo = memo || { }; + + if (document.createEvent) { + element.dispatchEvent(event); + } else { + element.fireEvent(event.eventType, event); + } + + return event; + } + }; +})()); + +Object.extend(Event, Event.Methods); + +Element.addMethods({ + fire: Event.fire, + observe: Event.observe, + stopObserving: Event.stopObserving +}); + +Object.extend(document, { + fire: Element.Methods.fire.methodize(), + observe: Element.Methods.observe.methodize(), + stopObserving: Element.Methods.stopObserving.methodize() +}); + +(function() { + /* Support for the DOMContentLoaded event is based on work by Dan Webb, + Matthias Miller, Dean Edwards and John Resig. */ + + var timer, fired = false; + + function fireContentLoadedEvent() { + if (fired) return; + if (timer) window.clearInterval(timer); + document.fire("dom:loaded"); + fired = true; + } + + if (document.addEventListener) { + if (Prototype.Browser.WebKit) { + timer = window.setInterval(function() { + if (/loaded|complete/.test(document.readyState)) + fireContentLoadedEvent(); + }, 0); + + Event.observe(window, "load", fireContentLoadedEvent); + + } else { + document.addEventListener("DOMContentLoaded", + fireContentLoadedEvent, false); + } + + } else { + document.write("