Permalink
Browse files

OS-1736 Allow creating / modifying firewall rules in the GZ

  • Loading branch information...
1 parent 5911c20 commit dc657c7c0507a1ec6354654486e1e648d1311481 @rgulewich rgulewich committed Dec 3, 2012
Showing with 18,505 additions and 0 deletions.
  1. +41 −0 overlay/generic/usr/lib/brand/joyent-minimal/statechange
  2. +41 −0 overlay/generic/usr/lib/brand/joyent/statechange
  3. +6 −0 src/Makefile
  4. +178 −0 src/fw/Makefile
  5. +43 −0 src/fw/README.md
  6. +41 −0 src/fw/etc/fwadm.completion
  7. +174 −0 src/fw/lib/cli.js
  8. +2,258 −0 src/fw/lib/fw.js
  9. +464 −0 src/fw/lib/fwadm.js
  10. +258 −0 src/fw/lib/ipf.js
  11. +76 −0 src/fw/lib/parser/index.js
  12. +405 −0 src/fw/lib/parser/parser.js
  13. +81 −0 src/fw/lib/parser/validators.js
  14. +68 −0 src/fw/lib/pipeline.js
  15. +200 −0 src/fw/lib/rule.js
  16. +120 −0 src/fw/lib/util/obj.js
  17. +95 −0 src/fw/lib/util/vm.js
  18. +196 −0 src/fw/node_modules/assert-plus.js
  19. +18 −0 src/fw/node_modules/clone/LICENSE
  20. +17 −0 src/fw/node_modules/clone/clonePrototype.js
  21. +112 −0 src/fw/node_modules/clone/index.js
  22. +165 −0 src/fw/node_modules/extsprintf.js
  23. +82 −0 src/fw/node_modules/mkdirp.js
  24. +257 −0 src/fw/node_modules/node-uuid.js
  25. +308 −0 src/fw/node_modules/vasync.js
  26. +157 −0 src/fw/node_modules/verror.js
  27. +140 −0 src/fw/npm-shrinkwrap.json
  28. +29 −0 src/fw/package.json
  29. +29 −0 src/fw/sbin/fwadm
  30. +141 −0 src/fw/src/fwrule.jison
  31. +330 −0 src/fw/test/lib/helpers.js
  32. +365 −0 src/fw/test/lib/mocks.js
  33. +106 −0 src/fw/test/node_modules/abbrev.js
  34. +19 −0 src/fw/test/node_modules/async/LICENSE
  35. +623 −0 src/fw/test/node_modules/async/index.js
  36. +13 −0 src/fw/test/node_modules/buffer-equal.js
  37. +116 −0 src/fw/test/node_modules/bunker.js
  38. +208 −0 src/fw/test/node_modules/burrito/index.js
  39. +24 −0 src/fw/test/node_modules/burrito/node_modules/traverse/LICENSE
  40. +267 −0 src/fw/test/node_modules/burrito/node_modules/traverse/index.js
  41. +286 −0 src/fw/test/node_modules/charm/index.js
  42. +18 −0 src/fw/test/node_modules/charm/lib/encode.js
  43. +84 −0 src/fw/test/node_modules/deep-equal.js
  44. +371 −0 src/fw/test/node_modules/difflet.js
  45. +29 −0 src/fw/test/node_modules/inherits/index.js
  46. +24 −0 src/fw/test/node_modules/mockery/LICENSE
  47. +275 −0 src/fw/test/node_modules/mockery/index.js
  48. +19 −0 src/fw/test/node_modules/nodeunit/LICENSE
  49. +133 −0 src/fw/test/node_modules/nodeunit/bin/nodeunit
  50. +10 −0 src/fw/test/node_modules/nodeunit/bin/nodeunit.json
  51. +623 −0 src/fw/test/node_modules/nodeunit/deps/async.js
  52. +55 −0 src/fw/test/node_modules/nodeunit/deps/console.log.js
  53. +70 −0 src/fw/test/node_modules/nodeunit/deps/ejs/History.md
  54. +20 −0 src/fw/test/node_modules/nodeunit/deps/ejs/Makefile
  55. +152 −0 src/fw/test/node_modules/nodeunit/deps/ejs/Readme.md
  56. +14 −0 src/fw/test/node_modules/nodeunit/deps/ejs/benchmark.js
  57. +531 −0 src/fw/test/node_modules/nodeunit/deps/ejs/ejs.js
  58. +2 −0 src/fw/test/node_modules/nodeunit/deps/ejs/ejs.min.js
  59. +5 −0 src/fw/test/node_modules/nodeunit/deps/ejs/examples/client.html
  60. +7 −0 src/fw/test/node_modules/nodeunit/deps/ejs/examples/list.ejs
  61. +16 −0 src/fw/test/node_modules/nodeunit/deps/ejs/examples/list.js
  62. +2 −0 src/fw/test/node_modules/nodeunit/deps/ejs/index.js
  63. +251 −0 src/fw/test/node_modules/nodeunit/deps/ejs/lib/ejs.js
  64. +198 −0 src/fw/test/node_modules/nodeunit/deps/ejs/lib/filters.js
  65. +23 −0 src/fw/test/node_modules/nodeunit/deps/ejs/lib/utils.js
  66. +11 −0 src/fw/test/node_modules/nodeunit/deps/ejs/package.json
  67. +173 −0 src/fw/test/node_modules/nodeunit/deps/ejs/support/compile.js
  68. +269 −0 src/fw/test/node_modules/nodeunit/deps/ejs/test/ejs.test.js
  69. +481 −0 src/fw/test/node_modules/nodeunit/deps/json2.js
  70. +3 −0 src/fw/test/node_modules/nodeunit/index.js
  71. +354 −0 src/fw/test/node_modules/nodeunit/lib/assert.js
  72. +318 −0 src/fw/test/node_modules/nodeunit/lib/core.js
  73. +104 −0 src/fw/test/node_modules/nodeunit/lib/nodeunit.js
  74. +121 −0 src/fw/test/node_modules/nodeunit/lib/reporters/browser.js
  75. +131 −0 src/fw/test/node_modules/nodeunit/lib/reporters/default.js
  76. +104 −0 src/fw/test/node_modules/nodeunit/lib/reporters/eclipse.js
  77. +110 −0 src/fw/test/node_modules/nodeunit/lib/reporters/html.js
  78. +14 −0 src/fw/test/node_modules/nodeunit/lib/reporters/index.js
  79. +180 −0 src/fw/test/node_modules/nodeunit/lib/reporters/junit.js
  80. +112 −0 src/fw/test/node_modules/nodeunit/lib/reporters/machineout.js
  81. +121 −0 src/fw/test/node_modules/nodeunit/lib/reporters/minimal.js
  82. +214 −0 src/fw/test/node_modules/nodeunit/lib/reporters/nested.js
  83. +108 −0 src/fw/test/node_modules/nodeunit/lib/reporters/skip_passed.js
  84. +65 −0 src/fw/test/node_modules/nodeunit/lib/reporters/tap.js
  85. +123 −0 src/fw/test/node_modules/nodeunit/lib/reporters/verbose.js
  86. +48 −0 src/fw/test/node_modules/nodeunit/lib/track.js
  87. +189 −0 src/fw/test/node_modules/nodeunit/lib/types.js
  88. +203 −0 src/fw/test/node_modules/nodeunit/lib/utils.js
  89. +23 −0 src/fw/test/node_modules/nopt/LICENSE
  90. +555 −0 src/fw/test/node_modules/nopt/index.js
  91. +127 −0 src/fw/test/node_modules/runforcover.js
  92. +1 −0 src/fw/test/node_modules/slide/index.js
  93. +65 −0 src/fw/test/node_modules/slide/lib/async-map-ordered.js
  94. +56 −0 src/fw/test/node_modules/slide/lib/async-map.js
  95. +16 −0 src/fw/test/node_modules/slide/lib/bind-actor.js
  96. +20 −0 src/fw/test/node_modules/slide/lib/chain.js
  97. +3 −0 src/fw/test/node_modules/slide/lib/slide.js
  98. +23 −0 src/fw/test/node_modules/tap/LICENSE
  99. +19 −0 src/fw/test/node_modules/tap/bin/tap-http.js
  100. +33 −0 src/fw/test/node_modules/tap/bin/tap-reader.js
  101. +132 −0 src/fw/test/node_modules/tap/bin/tap.js
  102. +16 −0 src/fw/test/node_modules/tap/lib/main.js
  103. +445 −0 src/fw/test/node_modules/tap/lib/tap-assert.js
  104. +63 −0 src/fw/test/node_modules/tap/lib/tap-browser-harness.js
  105. +245 −0 src/fw/test/node_modules/tap/lib/tap-consumer.js
  106. +78 −0 src/fw/test/node_modules/tap/lib/tap-cov-html.js
  107. +68 −0 src/fw/test/node_modules/tap/lib/tap-global-harness.js
  108. +223 −0 src/fw/test/node_modules/tap/lib/tap-harness.js
  109. +130 −0 src/fw/test/node_modules/tap/lib/tap-producer.js
  110. +71 −0 src/fw/test/node_modules/tap/lib/tap-results.js
  111. +453 −0 src/fw/test/node_modules/tap/lib/tap-runner.js
  112. +109 −0 src/fw/test/node_modules/tap/lib/tap-test.js
  113. +60 −0 src/fw/test/node_modules/tap/package.json
  114. +24 −0 src/fw/test/node_modules/traverse/LICENSE
  115. +310 −0 src/fw/test/node_modules/traverse/index.js
  116. +17 −0 src/fw/test/node_modules/uglify-js/index.js
  117. +75 −0 src/fw/test/node_modules/uglify-js/lib/object-ast.js
Sorry, we could not display the entire diff because it was too big.
View
41 overlay/generic/usr/lib/brand/joyent-minimal/statechange
@@ -377,6 +377,46 @@ setup_net()
}
#
+# Log a message, then exit
+#
+log_and_exit()
+{
+ echo "$1"
+ logger -p daemon.err "zone $ZONENAME $1"
+ exit 1
+}
+
+#
+# Set up the firewall for the zone.
+#
+setup_fw()
+{
+ ipf_conf=$ZONEPATH/config/ipf.conf
+ if [ -e $ipf_conf ]; then
+ echo "starting firewall ($ipf_conf)"
+ /usr/sbin/ipf -E $ZONENAME
+ if (( $? != 0 )); then
+ log_and_exit "error enabling ipfilter"
+ fi
+
+ /usr/sbin/ipf -Fa $ZONENAME
+ if (( $? != 0 )); then
+ log_and_exit "error flushing ipfilter"
+ fi
+
+ /usr/sbin/ipf -f $ipf_conf $ZONENAME
+ if (( $? != 0 )); then
+ log_and_exit "error loading ipfilter config"
+ fi
+
+ /usr/sbin/ipf -y $ZONENAME
+ if (( $? != 0 )); then
+ log_and_exit "error syncing ipfilter interfaces"
+ fi
+ fi
+}
+
+#
# We're readying the zone. Make sure the per-zone writable
# directories exist so that we can lofs mount them. We do this here,
# instead of in the install script, since this list has evolved and
@@ -601,6 +641,7 @@ load_sdc_config
if [[ "$subcommand" == "post" && $cmd == 0 ]]; then
setup_snapshots
setup_net
+ setup_fw
fi
# We can't set a rctl until we have a process in the zone to grab
View
41 overlay/generic/usr/lib/brand/joyent/statechange
@@ -377,6 +377,46 @@ setup_net()
}
#
+# Log a message, then exit
+#
+log_and_exit()
+{
+ echo "$1"
+ logger -p daemon.err "zone $ZONENAME $1"
+ exit 1
+}
+
+#
+# Set up the firewall for the zone.
+#
+setup_fw()
+{
+ ipf_conf=$ZONEPATH/config/ipf.conf
+ if [ -e $ipf_conf ]; then
+ echo "starting firewall ($ipf_conf)"
+ /usr/sbin/ipf -E $ZONENAME
+ if (( $? != 0 )); then
+ log_and_exit "error enabling ipfilter"
+ fi
+
+ /usr/sbin/ipf -Fa $ZONENAME
+ if (( $? != 0 )); then
+ log_and_exit "error flushing ipfilter"
+ fi
+
+ /usr/sbin/ipf -f $ipf_conf $ZONENAME
+ if (( $? != 0 )); then
+ log_and_exit "error loading ipfilter config"
+ fi
+
+ /usr/sbin/ipf -y $ZONENAME
+ if (( $? != 0 )); then
+ log_and_exit "error syncing ipfilter interfaces"
+ fi
+ fi
+}
+
+#
# We're readying the zone. Make sure the per-zone writable
# directories exist so that we can lofs mount them. We do this here,
# instead of in the install script, since this list has evolved and
@@ -601,6 +641,7 @@ load_sdc_config
if [[ "$subcommand" == "post" && $cmd == 0 ]]; then
setup_snapshots
setup_net
+ setup_fw
fi
# We can't set a rctl until we have a process in the zone to grab
View
6 src/Makefile
@@ -363,6 +363,12 @@ install: all
> $(DESTDIR)/usr/bin/bunyan
cp -PR img $(DESTDIR)/usr/
mkdir -m 0755 -p $(DESTDIR)/var/db/imgadm
+ rm -rf $(DESTDIR)/usr/fw
+ mkdir -m 0755 -p $(DESTDIR)/usr/fw
+ cp -PR fw/etc $(DESTDIR)/usr/fw/
+ cp -PR fw/lib $(DESTDIR)/usr/fw/
+ cp -PR fw/node_modules $(DESTDIR)/usr/fw/
+ cp -PR fw/sbin $(DESTDIR)/usr/fw/
check: $(JSLINT)
@echo "==> Running cstyle..."
View
178 src/fw/Makefile
@@ -0,0 +1,178 @@
+#
+# Copyright (c) 2012, Joyent, Inc. All rights reserved.
+#
+
+
+#
+# Directories
+#
+ROOT = $(PWD)/../..
+TOP := $(shell pwd)
+NM := node_modules
+NM_ORIG := node_modules.orig
+TEST_MODULES := test/node_modules
+
+
+#
+# Tools
+#
+NODEUNIT := $(TEST_MODULES)/nodeunit/bin/nodeunit
+JISON := ./tools/jison/lib/jison/cli-wrapper.js
+JSSTYLE = $(ROOT)/tools/jsstyle/jsstyle
+JSLINT = $(ROOT)/tools/javascriptlint/build/install/jsl
+
+#
+# Files
+#
+JS_CHECK_TARGETS=\
+ lib/*.js \
+ lib/util/*.js \
+ lib/parser/index.js \
+ lib/parser/validators.js \
+ sbin/* \
+ test/unit/*.js \
+ test/lib/*.js
+
+#
+# Tool options
+#
+JSSTYLE_OPTS = -o indent=2,strict-indent=1,doxygen,unparenthesized-return=0,continuation-at-front=1,leading-right-paren-ok=1
+
+#
+# Repo-specific targets
+#
+.PHONY: test
+test:
+ $(NODEUNIT) --reporter tap test/unit/*.js
+
+.PHONY: parser
+parser: $(JISON)
+ $(JISON) -o lib/parser/parser.js ./src/fwrule.jison
+
+# Create a node modules suitable for installing in the platform
+$(NM):
+ rm -rf $(NM)
+ npm install
+ rm -rf $(NM_ORIG)
+ mv node_modules{,.orig}
+ @mkdir $(NM)
+ @cp $(NM_ORIG)/assert-plus/assert.js $(NM)/assert-plus.js
+ @cp $(NM_ORIG)/node-uuid/uuid.js $(NM)/node-uuid.js
+ @cp $(NM_ORIG)/mkdirp/index.js $(NM)/mkdirp.js
+ @cp $(NM_ORIG)/extsprintf/lib/extsprintf.js \
+ $(NM_ORIG)/verror/lib/verror.js \
+ $(NM_ORIG)/vasync/lib/vasync.js \
+ $(NM)/
+ @mkdir $(NM)/clone
+ @cp $(NM_ORIG)/clone/clone.js $(NM)/clone/index.js
+ @cp $(NM_ORIG)/clone/{LICENSE,clonePrototype.js} $(NM)/clone/
+
+#
+# Re-adding Jison source files
+#
+JISON_FILES = lib package.json
+JISON_NM = tools/jison/node_modules
+JISON_NM_ORIG = $(NM_ORIG)/jison/node_modules
+
+$(JISON):
+ @rm -rf tools/jison
+ @mkdir tools/jison
+ @(for F in $(JISON_FILES); do \
+ cp -r $(NM_ORIG)/jison/$$F tools/jison/; \
+ done)
+ @mkdir -p $(JISON_NM)/JSONSelect
+ @cp $(JISON_NM_ORIG)/JSONSelect/src/jsonselect.js \
+ $(JISON_NM)/JSONSelect/index.js
+ @cp $(JISON_NM_ORIG)/JSONSelect/LICENSE \
+ $(JISON_NM)/JSONSelect/
+ @mkdir -p $(JISON_NM)/nomnom
+ @cp $(JISON_NM_ORIG)/nomnom/nomnom.js \
+ $(JISON_NM)/nomnom/index.js
+ @mkdir -p $(JISON_NM)/underscore
+ @cp $(JISON_NM_ORIG)/nomnom/node_modules/underscore/{index.js,underscore.js,LICENSE} \
+ $(JISON_NM)/underscore/
+ @mkdir -p $(JISON_NM)/reflect
+ @cp -r $(JISON_NM_ORIG)/reflect/{package.json,dist} \
+ $(JISON_NM)/reflect
+
+
+#
+# Re-adding nodeunit and test-related source files
+#
+NODEUNIT_FILES = LICENSE bin deps index.js lib
+TAP_FILES = LICENSE bin lib package.json
+TAP_NM = $(NM_ORIG)/nodeunit/node_modules/tap/node_modules
+# TAP dependencies that just have an index.js in their directory
+TAP_IDX_MODS = buffer-equal deep-equal difflet
+BUNKER = $(TAP_NM)/runforcover/node_modules/bunker
+BURRITO = $(BUNKER)/node_modules/burrito
+
+.PHONY: nodeunit
+nodeunit:
+ @rm -rf $(TEST_MODULES)
+ @mkdir -p $(TEST_MODULES)/nodeunit
+ @(for F in $(NODEUNIT_FILES); do \
+ cp -r $(NM_ORIG)/nodeunit/$$F $(TEST_MODULES)/nodeunit/; \
+ done)
+ @mkdir -p $(TEST_MODULES)/async
+ @cp $(TEST_MODULES)/nodeunit/deps/async.js $(TEST_MODULES)/async/index.js
+ @cp $(TEST_MODULES)/nodeunit/LICENSE $(TEST_MODULES)/async/
+ @mkdir -p $(TEST_MODULES)/tap
+ @(for F in $(TAP_FILES); do \
+ cp -r $(NM_ORIG)/nodeunit/node_modules/tap/$$F $(TEST_MODULES)/tap/; \
+ done)
+ @(for D in $(TAP_IDX_MODS); do \
+ cp -r $(TAP_NM)/$$D/index.js $(TEST_MODULES)/$$D.js; \
+ done)
+ @cp $(TAP_NM)/buffer-equal/index.js $(TEST_MODULES)/buffer-equal.js
+ @mkdir -p $(TEST_MODULES)/charm
+ @cp -r $(TAP_NM)/difflet/node_modules/charm/{index.js,lib} $(TEST_MODULES)/charm/
+ @cp $(TAP_NM)/deep-equal/index.js $(TEST_MODULES)/deep-equal.js
+ @mkdir -p $(TEST_MODULES)/inherits
+ @cp $(TAP_NM)/inherits/inherits.js $(TEST_MODULES)/inherits/index.js
+ @mkdir -p $(TEST_MODULES)/nopt
+ @cp $(TAP_NM)/nopt/lib/nopt.js $(TEST_MODULES)/nopt/index.js
+ @cp $(TAP_NM)/nopt/LICENSE $(TEST_MODULES)/nopt/
+ @cp $(TAP_NM)/nopt/node_modules/abbrev/lib/abbrev.js $(TEST_MODULES)/
+ @cp $(TAP_NM)/runforcover/index.js $(TEST_MODULES)/runforcover.js
+ @cp $(BUNKER)/index.js $(TEST_MODULES)/bunker.js
+ @mkdir -p $(TEST_MODULES)/traverse
+ @cp $(TAP_NM)/difflet/node_modules/traverse/{index.js,LICENSE} $(TEST_MODULES)/traverse
+ @mkdir -p $(TEST_MODULES)/burrito/node_modules/traverse
+ @cp $(BURRITO)/index.js $(TEST_MODULES)/burrito/
+ @cp $(BURRITO)/node_modules/traverse/{index.js,LICENSE} $(TEST_MODULES)/burrito/node_modules/traverse/
+ @mkdir -p $(TEST_MODULES)/uglify-js
+ @cp $(BURRITO)/node_modules/uglify-js/uglify-js.js $(TEST_MODULES)/uglify-js/index.js
+ @cp -r $(BURRITO)/node_modules/uglify-js/lib $(TEST_MODULES)/uglify-js/
+ @mkdir -p $(TEST_MODULES)/slide
+ @cp -r $(TAP_NM)/slide/{lib,index.js} $(TEST_MODULES)/slide/
+ @mkdir -p $(TEST_MODULES)/yamlish
+ @cp $(TAP_NM)/yamlish/yamlish.js $(TEST_MODULES)/yamlish/index.js
+ @cp $(TAP_NM)/yamlish/LICENSE $(TEST_MODULES)/yamlish/
+ @mkdir -p $(TEST_MODULES)/mockery
+ @cp $(NM_ORIG)/mockery/mockery.js $(TEST_MODULES)/mockery/index.js
+ @cp $(NM_ORIG)/mockery/LICENSE $(TEST_MODULES)/mockery/
+
+
+#
+# check and related
+#
+check: $(JSLINT) jslint jsstyle
+
+jslint:
+ @printf "\n==> Running JavaScriptLint...\n"
+ @$(JSLINT) --nologo --conf=$(TOP)/tools/jsl.node.conf \
+ $(JS_CHECK_TARGETS)
+
+jsstyle:
+ @printf "\n==> Running jsstyle...\n"
+ @# jsstyle doesn't echo as it goes so we add an echo to each line below
+ (for file in $(JS_CHECK_TARGETS); do \
+ echo $(PWD)/$$file; \
+ $(JSSTYLE) $(JSSTYLE_OPTS) $$file; \
+ [[ $$? == "0" ]] || exit 1; \
+ done)
+ @printf "\nJS style ok!\n"
+
+$(JSLINT):
+ (cd $(ROOT); $(MAKE) jsl)
View
43 src/fw/README.md
@@ -0,0 +1,43 @@
+# fwadm
+
+Repository: <https://github.com/joyent/smartos-live.git>
+Browsing: <https://github.com/joyent/smartos-live/tree/master/src/fw>
+Who: SmartOS mailing list <smartos-discuss@lists.smartos.org>
+Docs: <https://github.com/joyent/smartos-live/tree/master/src/fw/README.md>
+Tickets/bugs: <https://github.com/joyent/smartos-live/issues>
+
+
+# Overview
+
+fwadm is an adminstrative tool for managing VM firewalls.
+
+
+# Repository
+
+ etc/ Contains the bash completion file
+ lib/ Source files.
+ node_modules/ Committed node.js dependencies
+ sbin/ Executables that are runnable as root
+ src/ Contains the jison grammar for creating the firewall rule
+ parser
+ test/ Test suite (using nodeunit)
+ tools/ Dev tools, including jison for generating the rule parser
+
+
+# Development
+
+Before checking in, please run:
+
+ make check
+
+and fix any warnings. Note that jsstyle will stop after the first file with an
+error, so you may need to run this multiple times while fixing.
+
+
+# Testing
+
+ make test
+
+To run an individual test:
+
+ ./test/runtest <path to test file>
View
41 src/fw/etc/fwadm.completion
@@ -0,0 +1,41 @@
+_fwadm()
+{
+ local cur prev opts base
+ COMPREPLY=()
+ COMMANDS="list get add update enable disable start stop status rules"
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ prev="${COMP_WORDS[COMP_CWORD-1]}"
+
+ if [[ ${prev} == 'start'
+ || ${prev} == 'stop'
+ || ${prev} == 'status'
+ || ${prev} == 'rules'
+ ]] && [[ ${COMP_WORDS[COMP_CWORD-2]} == "fwadm" ]]; then
+
+ vms_uuids=$(zoneadm list -cp | grep -v ':global:' | cut -d':' -f5 | \
+ sort | uniq)
+ COMPREPLY=( $(compgen -W "${vms_uuids}" -- ${cur}) )
+
+ elif [[ ${prev} == 'enable'
+ || ${prev} == 'disable'
+ || ${prev} == 'get' ]]; then
+
+ rule_uuids=$(/usr/bin/ls -1 /var/fw/rules/*.json | \
+ sed -e 's,/var/fw/,,' -e 's/.json//')
+ COMPREPLY=( $(compgen -W "${rule_uuids}" -- ${cur}) )
+
+ elif [[ ${prev} == 'fwadm' ]]; then
+
+ COMPREPLY=( $(compgen -W "${COMMANDS}" -- ${cur}) )
+
+ elif [[ ${prev} == '-f' ]]; then
+
+ # Just expand files by default
+ COMPREPLY=( $(compgen -f -- ${cur}) )
+
+ fi
+
+ return 0
+}
+
+complete -F _fwadm fwadm
View
174 src/fw/lib/cli.js
@@ -0,0 +1,174 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License"). You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at http://smartos.org/CDDL
+ *
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file.
+ *
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ * Copyright (c) 2012, Joyent, Inc. All rights reserved.
+ *
+ * fwadm: CLI shared logic
+ */
+
+var fs = require('fs');
+var tty = require('tty');
+var util = require('util');
+var verror = require('verror');
+
+
+
+// --- Globals
+
+
+
+var UUID_REGEX =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+
+
+
+// --- Exported functions
+
+
+
+/**
+ * Displays a list of firewall rules
+ */
+function displayRules(err, res, opts) {
+ if (err) {
+ return outputError(err, opts);
+ }
+
+ if (opts.json) {
+ return console.log(json(res));
+ }
+
+ console.log('UUID ENABLED RULE');
+ res.forEach(function (r) {
+ console.log(ruleLine(r));
+ });
+}
+
+
+/**
+ * Reads the payload from one of: a file, stdin, a text argument
+ */
+function getPayload(opts, callback) {
+ if (!opts.file && !tty.isatty(0)) {
+ opts.file = '-';
+ }
+
+ if (!opts.file) {
+ return callback(new verror.VError('Must supply file!'));
+ }
+
+ if (opts.file === '-') {
+ opts.file = '/dev/stdin';
+ }
+
+ fs.readFile(opts.file, function (err, data) {
+ if (err) {
+ if (err.code === 'ENOENT') {
+ return callback(new verror.VError(
+ 'File "%s" does not exist.', opts.file));
+ }
+ return callback(new verror.VError(
+ 'Error reading "%s": %s', opts.file, err.message));
+ }
+
+ return callback(null, JSON.parse(data.toString()));
+ });
+}
+
+
+/**
+ * Pretty-print a JSON object
+ */
+function json(obj) {
+ return JSON.stringify(obj, null, 2);
+}
+
+
+/**
+ * Outputs an error to the console, displaying all of the error messages
+ * if it's a MultiError
+ */
+function outputError(err, opts) {
+ var errs = [ err ];
+ if (err.hasOwnProperty('ase_errors')) {
+ errs = err.ase_errors;
+ }
+
+ if (opts && opts.json) {
+ return console.error(json({
+ errors: errs.map(function (e) {
+ var j = { message: e.message };
+ if (e.hasOwnProperty('code')) {
+ j.code = e.code;
+ }
+
+ if (opts.verbose) {
+ j.stack = e.stack;
+ }
+ return j;
+ })
+ }));
+ }
+
+ errs.forEach(function (e) {
+ console.error(e.message);
+ if (opts.verbose) {
+ console.error(e.stack);
+ }
+ });
+}
+
+
+/**
+ * Outputs one formatted rule line
+ */
+function ruleLine(r) {
+ return util.format('%s %s %s', r.uuid,
+ r.enabled ? 'true ' : 'false ', r.rule);
+}
+
+
+/**
+ * Prints an error and exits if the UUID is invalid
+ */
+function validateUUID(arg) {
+ if (!arg) {
+ console.error('Error: missing UUID');
+ process.exit(1);
+ }
+ if (!UUID_REGEX.test(arg)) {
+ console.error('Error: invalid UUID "%s"', arg);
+ process.exit(1);
+ }
+ return arg;
+}
+
+
+
+module.exports = {
+ displayRules: displayRules,
+ getPayload: getPayload,
+ json: json,
+ outputError: outputError,
+ ruleLine: ruleLine,
+ validateUUID: validateUUID
+};
View
2,258 src/fw/lib/fw.js
@@ -0,0 +1,2258 @@
+/*
+ * CDDL HEADER START
+ *
+ * The contents of this file are subject to the terms of the
+ * Common Development and Distribution License, Version 1.0 only
+ * (the "License"). You may not use this file except in compliance
+ * with the License.
+ *
+ * You can obtain a copy of the license at http://smartos.org/CDDL
+ *
+ * See the License for the specific language governing permissions
+ * and limitations under the License.
+ *
+ * When distributing Covered Code, include this CDDL HEADER in each
+ * file.
+ *
+ * If applicable, add the following below this CDDL HEADER, with the
+ * fields enclosed by brackets "[]" replaced with your own identifying
+ * information: Portions Copyright [yyyy] [name of copyright owner]
+ *
+ * CDDL HEADER END
+ *
+ * Copyright (c) 2012, Joyent, Inc. All rights reserved.
+ *
+ * fwadm: Main entry points
+ */
+
+var assert = require('assert-plus');
+var bunyan = require('/usr/node/node_modules/bunyan');
+var clone = require('clone');
+var fs = require('fs');
+var mkdirp = require('mkdirp');
+var mod_ipf = require('./ipf');
+var mod_obj = require('./util/obj');
+var mod_rule = require('./rule');
+var pipeline = require('./pipeline').pipeline;
+var sprintf = require('extsprintf').sprintf;
+var util = require('util');
+var util_vm = require('./util/vm');
+var vasync = require('vasync');
+var verror = require('verror');
+
+var createSubObjects = mod_obj.createSubObjects;
+var forEachKey = mod_obj.forEachKey;
+var objEmpty = mod_obj.objEmpty;
+var mergeObjects = mod_obj.mergeObjects;
+
+
+
+// --- Globals
+
+
+
+var DIRECTIONS = ['from', 'to'];
+var LOG;
+var LOG_DIR = '/var/log/fw';
+var RULE_PATH = '/var/fw/rules';
+var VM_PATH = '/var/fw/vms';
+var IPF_CONF = '%s/config/ipf.conf';
+var IPF_CONF_OLD = '%s/config/ipf.conf.old';
+
+
+
+// --- Internal helper functions
+
+
+
+/**
+ * Adds to a 3-level deep object
+ */
+function addToHash3(hash, key1, key2, key3, obj) {
+ if (!hash.hasOwnProperty(key1)) {
+ hash[key1] = {};
+ }
+ if (!hash[key1].hasOwnProperty(key2)) {
+ hash[key1][key2] = {};
+ }
+ if (!hash[key1][key2].hasOwnProperty(key3)) {
+ hash[key1][key2][key3] = obj;
+ }
+}
+
+
+/**
+ * For a rule and a direction, return whether or not we actually need to
+ * write ipf rules. FROM+ALLOW and TO+BLOCK are essentially no-ops, as
+ * they will be caught by the block / allow catch-all default rules.
+ */
+function noRulesNeeded(dir, rule) {
+ if ((dir === 'from' && rule.action === 'allow')
+ || (dir === 'to' && rule.action === 'block')) {
+ return true;
+ }
+ return false;
+}
+
+
+/**
+ * For each rule in rules, call cb for each target present in the rule,
+ * passing the rule, target type and target itself.
+ *
+ * @param rules {Array} : rule objects to process
+ * @param types {Array} : (optional)
+ */
+function ruleTypeWalk(rules, types, cb) {
+ if (typeof (types) === 'function') {
+ cb = types;
+ types = ['ips', 'tags', 'machines'];
+ }
+
+ rules.forEach(function (rule) {
+ types.forEach(function (type) {
+ rule[type].forEach(function (t) {
+ cb(rule, type, t);
+ });
+ });
+ });
+}
+
+
+/**
+ * For each rule in rules, call cb for each target present in the rule,
+ * passing the rule, direction, target type and target itself.
+ *
+ * @param rules {Array} : rule objects to process
+ */
+function ruleTypeDirWalk(rules, cb) {
+ rules.forEach(function (rule) {
+ DIRECTIONS.forEach(function (dir) {
+ ['ips', 'tags', 'machines'].forEach(function (type) {
+ if (rule[dir].hasOwnProperty(type)) {
+ rule[dir][type].forEach(function (t) {
+ cb(rule, dir, type, t);
+ });
+ }
+ });
+ });
+ });
+}
+
+
+/**
+ * Returns a list of rules with duplicates removed. Rules in list1 will
+ * override rules in list2
+ */
+function dedupRules(list1, list2) {
+ var seenUUIDs = {};
+ var toReturn = [];
+
+ list1.concat(list2).forEach(function (r) {
+ if (r.hasOwnProperty('uuid') && !seenUUIDs.hasOwnProperty(r.uuid)) {
+ toReturn.push(r);
+ seenUUIDs[r.uuid] = 1;
+ }
+ });
+
+ return toReturn;
+}
+
+
+/**
+ * Starts ipf and reloads the rules for a VM
+ */
+function startIPF(opts, callback) {
+ var ipfConf = util.format(IPF_CONF, opts.zonepath);
+
+ return mod_ipf.start(opts.vm, LOG, function (err) {
+ if (err) {
+ return callback(err);
+ }
+ return mod_ipf.reload(opts.vm, ipfConf, LOG, callback);
+ });
+}
+
+
+/**
+ * Generates a version string
+ */
+function newVersion() {
+ return Date.now(0) + '.' + sprintf('%06d', process.pid);
+}
+
+
+
+// --- Internal functions
+
+
+
+/*
+ * Create logger
+ */
+function createLogger(opts) {
+ if (LOG) {
+ return;
+ }
+
+ var logName = 'fwadm';
+ var logLevel = 'debug';
+
+ if (opts) {
+ // XXX: allow logging to stderr
+ if (opts.logName) {
+ logName = opts.logName;
+ }
+ if (opts.logLevel) {
+ logLevel = opts.logLevel;
+ }
+ }
+
+ mkdirp.sync(LOG_DIR);
+ var filename = util.format('%s/%s-%s-%s.log',
+ LOG_DIR, Date.now(0), sprintf('%06d', process.pid), logName);
+
+ var streams = [
+ {
+ level: logLevel,
+ path: filename
+ }
+ ];
+
+ LOG = bunyan.createLogger({
+ name: 'fw',
+ serializers: bunyan.stdSerializers,
+ streams: streams
+ });
+}
+
+
+/**
+ * Validates the payload passed to the exported functions. Throws an error
+ * if not in the right format
+ */
+function validateOpts(opts) {
+ assert.object(opts, 'opts');
+ assert.arrayOfObject(opts.vms, 'opts.vms');
+}
+
+
+/**
+ * Create rule objects from the rules
+ *
+ * @param {Array} inRules : raw rule input objects to create
+ * @param {Function} callback : of the form f(err, newRules)
+ */
+function createRules(inRules, callback) {
+ var errors = [];
+ var rules = [];
+ var ver = newVersion();
+
+ if (!inRules || inRules.length === 0) {
+ return callback(null, []);
+ }
+
+ inRules.forEach(function (payloadRule) {
+ var rule = clone(payloadRule);
+ if (!rule.hasOwnProperty('version')) {
+ rule.version = ver;
+ }
+
+ try {
+ var r = mod_rule.create(rule);
+ rules.push(r);
+ } catch (err) {
+ errors.push(err);
+ }
+ });
+
+ if (errors.length !== 0) {
+ return callback(new verror.MultiError(errors));
+ }
+
+ return callback(null, rules);
+}
+
+
+/**
+ * Merge updates from the rules in payload, and return the updated
+ * rule objects
+ */
+function createUpdatedRules(rules, payload, callback) {
+ LOG.trace('createUpdatedRules: entry');
+ if (!payload.rules || payload.rules.length === 0) {
+ return callback(null, []);
+ }
+
+ var updates = {};
+
+ payload.rules.forEach(function (r) {
+ updates[r.uuid] = r;
+ });
+
+ LOG.debug({payloadRules: updates, rules: rules },
+ 'createUpdatedRules: merging rules');
+
+ var updatedRules = rules.map(function (rule) {
+ return mergeObjects(updates[rule.uuid], rule.serialize());
+ });
+
+ LOG.debug(updatedRules, 'createUpdatedRules: rules merged');
+ return createRules(updatedRules, callback);
+}
+
+
+/**
+ * Turns a list of VMs from VM.js into a lookup table, keyed by the various
+ * properties we'd like to filter VMs by (tags, ips, and machines),
+ * like so:
+ * {
+ * all: { uuid1: <vm 1> }
+ * tags: { tag2: <vm 2> }
+ * machines: { uuid1: <vm 1> }
+ * ips: { 10.0.0.1: <vm 3> }
+ * ips: { 10.0.0.1: <vm 3> }
+ * }
+ */
+function createVMlookup(vms, callback) {
+ LOG.trace('createVMlookup: entry');
+
+ var vmStore = {
+ all: {},
+ ips: {},
+ machines: {},
+ subnets: {},
+ tags: {}
+ };
+
+ vms.forEach(function (fullVM) {
+ var vm = {
+ enabled: fullVM.firewall_enabled || false,
+ ips: fullVM.nics.map(function (n) { return (n.ip); }).filter(
+ function (i) { return (i !== 'dhcp'); }),
+ owner_uuid: fullVM.owner_uuid,
+ state: fullVM.state,
+ tags: Object.keys(fullVM.tags),
+ uuid: fullVM.uuid,
+ zonepath: fullVM.zonepath
+ };
+ LOG.trace(vm, 'Adding VM "%s" to lookup', vm.uuid);
+
+ vmStore.all[vm.uuid] = vm;
+ addToHash3(vmStore, 'machines', vm.uuid, vm.uuid, vm);
+
+ vm.tags.forEach(function (tag) {
+ addToHash3(vmStore, 'tags', tag, vm.uuid, vm);
+ });
+ vm.ips.forEach(function (ip) {
+ addToHash3(vmStore, 'ips', ip, vm.uuid, vm);
+ });
+ // XXX: subnet
+ });
+
+ if (LOG.debug()) {
+ var truncated = { };
+ ['machines', 'tags', 'ips'].forEach(function (type) {
+ truncated[type] = {};
+ if (!vmStore.hasOwnProperty(type)) {
+ return;
+ }
+
+ Object.keys(vmStore[type]).forEach(function (t) {
+ truncated[type][t] = Object.keys(vmStore[type][t]);
+ });
+ });
+
+ LOG.debug(truncated, 'vmStore');
+ }
+
+ return callback(null, vmStore);
+}
+
+
+/**
+ * Create a lookup table for remote VMs
+ */
+function createRemoteVMlookup(remoteVMs, callback) {
+ LOG.trace('createRemoteVMlookup: entry');
+
+ var remoteVMlookup = {
+ ips: {},
+ machines: {},
+ subnets: {},
+ tags: {}
+ };
+
+ if (!remoteVMs || objEmpty(remoteVMs)) {
+ return callback(null, remoteVMlookup);
+ }
+
+ var rvmList = remoteVMs;
+ if (!mod_obj.isArray(rvmList)) {
+ rvmList = [ remoteVMs ];
+ }
+
+ rvmList.forEach(function (rvmObj) {
+ forEachKey(rvmObj, function (uuid, rvm) {
+ // Make machines match the layout of tags, eg: tags[key][uuid] = { obj }
+ remoteVMlookup.machines[uuid] = {};
+ remoteVMlookup.machines[uuid][uuid] = rvm;
+
+ if (rvm.hasOwnProperty('tags')) {
+ for (var t in rvm.tags) {
+ createSubObjects(remoteVMlookup.tags, t, uuid, rvm);
+ }
+ }
+ });
+ });
+
+ return callback(null, remoteVMlookup);
+}
+
+
+/**
+ * Load a single rule from disk, returning a rule object
+ *
+ * @param {String} file : file to load the rule from
+ * @param {Function} callback : of the form f(err, rule)
+ * - Where vm is a rule object
+ */
+function loadRule(file, callback) {
+ LOG.debug('loadRule: loading rule file "%s"', file);
+ return fs.readFile(file, function (err, raw) {
+ if (err) {
+ return callback(err);
+ }
+ var rule;
+
+ try {
+ var parsed = JSON.parse(raw);
+ LOG.trace(parsed, 'loadRule: loaded rule file "%s"', file);
+ // XXX: validate that the rule has a uuid
+ rule = mod_rule.create(parsed);
+ } catch (err2) {
+ LOG.error(err2, 'loadRule: error creating rule');
+ return callback(err2);
+ }
+
+ if (LOG.trace()) {
+ LOG.trace(rule.toString(), 'loadRule: created rule');
+ }
+
+ return callback(null, rule);
+ });
+}
+
+
+/**
+ * Loads all rules from disk
+ */
+function loadAllRules(callback) {
+ var rules = [];
+
+ fs.readdir(RULE_PATH, function (err, files) {
+ if (err) {
+ if (err.code === 'ENOENT') {
+ return callback(null, []);
+ }
+ return callback(err);
+ }
+
+ return vasync.forEachParallel({
+ inputs: files,
+ func: function (file, cb) {
+ if (file.indexOf('.json', file.length - 5) === -1) {
+ return cb(null);
+ }
+
+ var rpath = util.format('%s/%s', RULE_PATH, file);
+ return loadRule(rpath, function (err2, rule) {
+ if (rule) {
+ rules.push(rule);
+ }
+ return cb(err2);
+ });
+ }
+ }, function (err3, res) {
+ return callback(err3, rules);
+ });
+ });
+}
+
+
+/*
+ * Saves rules to disk
+ *
+ * @param {Array} rules : rule objects to save
+ * @param {Function} callback : of the form f(err)
+ */
+function saveRules(rules, callback) {
+ var uuids = [];
+ var versions = {};
+ if (LOG.debug()) {
+ LOG.debug(rules.map(function (r) { return r.uuid; }),
+ 'saveRules: entry');
+ }
+
+ return vasync.pipeline({
+ funcs: [
+ function _mkdir(_, cb) { mkdirp(RULE_PATH, cb); },
+ function _writeRules(_, cb) {
+ return vasync.forEachParallel({
+ inputs: rules,
+ func: function _writeRule(rule, cb2) {
+ var ser = rule.serialize();
+ // XXX: allow overriding version in the payload
+ var filename = util.format('%s/%s.json.%s', RULE_PATH,
+ rule.uuid, rule.version);
+ LOG.trace(ser, 'writing "%s"', filename);
+
+ return fs.writeFile(filename, JSON.stringify(ser, null, 2),
+ function (err) {
+ if (err) {
+ return cb2(err);
+ }
+ uuids.push(rule.uuid);
+ versions[rule.uuid] = rule.version;
+
+ return cb2(null);
+ });
+ }
+ // XXX: if there are failures here, we want to delete these files
+ }, cb);
+ },
+ function _renameRules(_, cb) {
+ return vasync.forEachParallel({
+ inputs: uuids,
+ func: function _renameRule(uuid, cb2) {
+ var before = util.format('%s/%s.json.%s', RULE_PATH, uuid,
+ versions[uuid]);
+ var after = util.format('%s/%s.json', RULE_PATH, uuid);
+ LOG.trace('renaming "%s" to "%s"', before, after);
+ fs.rename(before, after, cb2);
+ }
+ }, cb);
+ }
+ ]}, callback);
+}
+
+
+/*
+ * Deletes rules on disk
+ *
+ * @param {Array} rules : rule objects to delete
+ * @param {Function} callback : of the form f(err)
+ */
+function deleteRules(rules, callback) {
+ if (LOG.debug()) {
+ LOG.debug(rules.map(function (r) { return r.uuid; }),
+ 'deleteRules: entry');
+ }
+
+ return vasync.forEachParallel({
+ inputs: rules.map(function (r) { return r.uuid; }),
+ func: function _delRule(uuid, cb) {
+ var filename = util.format('%s/%s.json', RULE_PATH, uuid);
+ LOG.trace('deleting "%s"', filename);
+
+ fs.unlink(filename, function (err) {
+ if (err && err.code == 'ENOENT') {
+ return cb();
+ }
+
+ return cb(err);
+ });
+ }
+ }, callback);
+}
+
+
+/**
+ * Load a single remote VM from disk, returning the object
+ *
+ * @param {String} file : file to load the remote VM from
+ * @param {Function} callback : of the form f(err, vm)
+ * - Where vm is a remote VM object
+ */
+function loadRemoteVM(file, callback) {
+ LOG.trace('loadRemoteVM: loading file "%s"', file);
+
+ return fs.readFile(file, function (err, raw) {
+ if (err) {
+ return callback(err);
+ }
+ var parsed;
+
+ try {
+ parsed = JSON.parse(raw);
+ LOG.trace(parsed, 'loadRemoteVM: loaded rule file "%s"', file);
+ // XXX: validate that the VM has a uuid
+ } catch (err2) {
+ LOG.error(err2, 'loadRemoteVM: error parsing VM file "%s"', file);
+ return callback(err2);
+ }
+
+ if (LOG.trace()) {
+ LOG.trace(parsed, 'loadRemoteVM: created rule');
+ }
+
+ return callback(null, parsed);
+ });
+}
+
+
+/**
+ * Loads all remote VMs from disk
+ *
+ * @param {Function} callback : of the form f(err, vms)
+ * - Where vms is an object containing the remote VMs, keyed by UUID
+ */
+function loadAllRemoteVMs(callback) {
+ var vms = {};
+
+ fs.readdir(VM_PATH, function (err, files) {
+ if (err) {
+ if (err.code === 'ENOENT') {
+ return callback(null, {});
+ }
+ return callback(err);
+ }
+
+ return vasync.forEachParallel({
+ inputs: files,
+ func: function (file, cb) {
+ if (file.indexOf('.json', file.length - 5) === -1) {
+ return cb(null);
+ }
+ var uuid = file.split('.')[0];
+
+ var path = util.format('%s/%s', VM_PATH, file);
+ return loadRemoteVM(path, function (err2, rvm) {
+ if (rvm) {
+ vms[uuid] = rvm;
+ }
+ return cb(err2);
+ });
+ }
+ }, function (err3, res) {
+ return callback(err3, vms);
+ });
+ });
+}
+
+
+/*
+ * Saves remote VMs to disk
+ *
+ * @param {Object} vms : remote VM objects to save, keyed by UUID
+ * @param {Function} callback : of the form f(err)
+ */
+function saveRemoteVMs(vms, callback) {
+ LOG.trace('saveRemoteVMs: entry');
+
+ if (!vms || objEmpty(vms)) {
+ return callback();
+ }
+
+ var uuids = [];
+ // XXX: allow overriding version in the payload
+ var versions = {};
+ var ver = newVersion();
+
+ return vasync.pipeline({
+ funcs: [
+ function _mkdir(_, cb) { mkdirp(VM_PATH, cb); },
+ function _writeVMs(_, cb) {
+ return vasync.forEachParallel({
+ inputs: Object.keys(vms),
+ func: function _writeVM(uuid, cb2) {
+ var vm = vms[uuid];
+ var filename = util.format('%s/%s.json.%s', VM_PATH, uuid, ver);
+ LOG.trace(vm, 'writing "%s"', filename);
+
+ return fs.writeFile(filename, JSON.stringify(vm, null, 2),
+ function (err) {
+ if (err) {
+ return cb2(err);
+ }
+
+ uuids.push(uuid);
+ versions[uuid] = ver;
+
+ return cb2(null);
+ });
+ }
+ // XXX: if there are failures here, we want to delete these files
+ }, cb);
+ },
+ function _renameRules(_, cb) {
+ return vasync.forEachParallel({
+ inputs: uuids,
+ func: function _renameRule(uuid, cb2) {
+ var before = util.format('%s/%s.json.%s', VM_PATH, uuid,
+ versions[uuid]);
+ var after = util.format('%s/%s.json', VM_PATH, uuid);
+ LOG.trace('renaming "%s" to "%s"', before, after);
+ fs.rename(before, after, cb2);
+ }
+ }, cb);
+ }
+ ]}, callback);
+}
+
+
+/**
+ * Finds rules in the list, returning an error if they can't be found
+ */
+function findRules(allRules, rules, callback) {
+ LOG.trace('findRules: entry');
+
+ if (!rules || rules.length === 0) {
+ return callback(null, []);
+ }
+
+ var errs = [];
+ var found = [];
+ var uuids = {};
+ rules.forEach(function (r) {
+ if (!r.hasOwnProperty('uuid')) {
+ errs.push(new verror.VError('Missing UUID of rule: %j', r));
+ return;
+ }
+ uuids[r.uuid] = 1;
+ });
+ LOG.debug(uuids, 'findRules: rules');
+
+ allRules.forEach(function (r) {
+ if (!r.hasOwnProperty('uuid')) {
+ errs.push(new verror.VError('Missing UUID of rule: %j', r));
+ }
+
+ if (uuids.hasOwnProperty(r.uuid)) {
+ delete uuids[r.uuid];
+ found.push(r);
+ }
+ });
+
+ if (!objEmpty(uuids)) {
+ Object.keys(uuids).forEach(function (uuid) {
+ errs.push(new verror.VError('Unknown rule: %s', uuid));
+ });
+ }
+
+ if (LOG.debug()) {
+ LOG.debug({ found: found.map(function (r) { return r.uuid; }),
+ missing: Object.keys(uuids)
+ }, 'findRules: return');
+ }
+
+ if (errs.length !== 0) {
+ return callback(new verror.MultiError(errs));
+ }
+
+ return callback(null, found);
+}
+
+
+/**
+ * Returns an object of the VMs the given rules apply to
+ *
+ * @param vms {Object}: VM lookup table, as returned by createVMlookup()
+ * @param rules {Array}: array of rule objects
+ * @param callback {Function} `function (err, matchingVMs)`
+ * - Where matchingVMs contains VM objects keyed by uuid, like:
+ * { vm_uuid: vmObj }
+ */
+function filterVMsByRules(vms, rules, callback) {
+ var matchingVMs = {};
+
+ ruleTypeWalk(rules, function _matchingVMs(rule, type, t) {
+ if (!vms[type].hasOwnProperty(t)) {
+ LOG.trace('filterVMsByRules: type=%s, t=%s, rule=%s: not in VM hash',
+ type, t, rule);
+ return;
+ }
+
+ var owner_uuid = rule.owner_uuid;
+
+ Object.keys(vms[type][t]).forEach(function (uuid) {
+ var vm = vms[type][t][uuid];
+ if (owner_uuid && vm.owner_uuid != owner_uuid) {
+ LOG.trace('filterVMsByRules: type=%s, t=%s, VM=%s: rule owner uuid'
+ + ' (%s) did not match VM owner uuid (%s): %s',
+ type, t, uuid, owner_uuid, vm.owner_uuid, rule);
+ return;
+ }
+ LOG.trace('filterVMsByRules: type=%s, t=%s, VM=%s: matched rule: %s',
+ type, t, uuid, rule);
+ matchingVMs[uuid] = vm;
+ });
+ });
+
+ if (LOG.debug()) {
+ var uuids = Object.keys(matchingVMs);
+ LOG.debug(uuids, 'filterVMsByRules: found %d matching VMs',
+ uuids.length);
+ }
+
+ return callback(null, matchingVMs);
+}
+
+
+/**
+ * Filter the list of rules, returning only the rules that contain VMs
+ * in the given remote VM lookup table
+ *
+ * @param remoteVMs {Object}: remote VM lookup table, as returned by
+ * createRemoteVMlookup()
+ * @param rules {Array}: array of rule objects
+ * @param callback {Function} `function (err, matchingRules)`
+ *
+ */
+function filterRulesByRemoteVMs(remoteVMs, rules, callback) {
+ LOG.trace('filterRulesByRemoteVMs: entry');
+
+ if (!remoteVMs || objEmpty(remoteVMs)) {
+ return callback(null, []);
+ }
+
+ var matchingRules = [];
+
+ // XXX: filter by owner_uuid here
+ ruleTypeWalk(rules, ['tags', 'machines'], function (rule, type, t) {
+ if (remoteVMs[type].hasOwnProperty(t)) {
+ matchingRules.push(rule);
+ }
+ return;
+ });
+
+ return callback(null, matchingRules);
+}
+
+
+/**
+ * Find rules that match a set of UUIDs. Warns if any of the UUIDs can't be
+ * found.
+ *
+ * @param rules {Array} : list of rules to filter
+ * @param uuids {Array} : UUIDs of rules to filter
+ * @param callback {Function} : `function (err, rules)`
+ * - Where matching is an object:
+ * { matching: [ <rules> ], other: [ <rules> ] }
+ */
+function filterRulesByUUIDs(rules, uuids, callback) {
+ LOG.debug(uuids, 'filterRulesByUUIDs: entry');
+ var results = {
+ matching: [],
+ notMatching: []
+ };
+ var uuidHash = uuids.reduce(function (acc, u) {
+ acc[u] = 1;
+ return acc;
+ }, {});
+
+ rules.forEach(function (rule) {
+ if (uuidHash.hasOwnProperty(rule.uuid)) {
+ delete uuidHash[rule.uuid];
+ results.matching.push(rule);
+ } else {
+ results.notMatching.push(rule);
+ }
+ });
+
+ if (!objEmpty(uuidHash)) {
+ LOG.warn(Object.keys(uuidHash), 'Trying to delete unknown rules');
+ }
+
+ return callback(null, results);
+}
+
+
+/**
+ * Find rules that apply to a set of VMs
+ *
+ * @param allVMs {Object} : VM lookup table
+ * @param vms {Object} : hash of VM UUIDs to find rules for
+ * - e.g. : { uuid1: 1, uuid2: 2 }
+ * @param rules {Array} : list of rules to filter
+ * @param callback {Function} : `function (err, matching)`
+ * - Where matching is an array of the matching rule objects
+ */
+function filterRulesByVMs(allVMs, vms, rules, callback) {
+ LOG.debug(vms, 'filterRulesByVMs: entry');
+ var matchingRules = [];
+
+ ruleTypeWalk(rules, function _filterByVM(rule, type, t) {
+ LOG.trace('filterRulesByVMs: type=%s, t=%s, rule=%s',
+ type, t, rule);
+ if (!allVMs[type].hasOwnProperty(t)) {
+ return;
+ }
+
+ var owner_uuid = rule.owner_uuid;
+
+ for (var uuid in allVMs[type][t]) {
+ if (!vms.hasOwnProperty(uuid)) {
+ continue;
+ }
+
+ if (owner_uuid && allVMs[type][t][uuid].owner_uuid != owner_uuid) {
+ LOG.trace('filterRulesByVMs: VM %s owner_uuid=%s does not match '
+ + 'rule owner_uuid=%s: %s', allVMs[type][t][uuid].owner_uuid,
+ owner_uuid, rule);
+ continue;
+ }
+
+ matchingRules.push(rule);
+ return;
+ }
+ });
+
+ if (LOG.debug()) {
+ LOG.debug(matchingRules.map(function (r) { return r.uuid; }),
+ 'filterRulesByVMs: %d matching rules', matchingRules.length);
+ }
+
+ return callback(null, matchingRules);
+}
+
+
+/**
+ * Looks up the given VMs in the VM lookup object, and returns an
+ * object mapping UUIDs to VM lookup objects
+ */
+function lookupVMs(allVMs, vms, callback) {
+ LOG.trace(vms, 'lookupVMs: entry');
+ if (!vms || vms.length === 0) {
+ return callback(null, {});
+ }
+
+ var toReturn = {};
+ var errs = [];
+ vms.forEach(function (vm) {
+ if (!vm.hasOwnProperty('uuid')) {
+ errs.push(new Error('VM missing uuid property'));
+ return;
+ }
+ if (!allVMs.all.hasOwnProperty(vm.uuid)) {
+ errs.push(new verror.VError('Could not find VM "%s" in VM list'));
+ return;
+ }
+ toReturn[vm.uuid] = allVMs.all[vm.uuid];
+ });
+
+ if (errs.length !== 0) {
+ return callback(new verror.MultiError(errs));
+ }
+
+ return callback(null, toReturn);
+}
+
+
+/**
+ * Validates the list of rules, ensuring that there's enough information
+ * to write each rule to disk
+ *
+ * @param vms {Object}: VM lookup table, as returned by createVMlookup()
+ * @param rvms {Object}: remote VM lookup table, as returned by
+ * createRemoteVMlookup()
+ * @param rules {Array}: array of rule objects
+ * @param callback {Function} `function (err)`
+ */
+function validateRules(vms, rvms, rules, callback) {
+ LOG.trace(rules, 'validateRules: entry');
+ var sideData = {};
+ var errs = [];
+ var rulesLeft = rules.reduce(function (h, r) {
+ h[r.uuid] = r;
+ return h;
+ }, {});
+
+ // XXX: make owner uuid aware
+
+ // First go through the rules finding all the VMs we need rules for
+ ruleTypeDirWalk(rules, function _getRuleData(rule, dir, type, t) {
+ // XXX: for now
+ if (type == 'ips') {
+ return;
+ }
+
+ createSubObjects(sideData, rule.uuid, dir, 'missing', type);
+ createSubObjects(sideData, rule.uuid, dir, 'vms');
+
+ if (vms[type].hasOwnProperty(t)) {
+ for (var vm in vms[type][t]) {
+ sideData[rule.uuid][dir].vms[vm] = 1;
+ }
+ delete rulesLeft[rule.uuid];
+
+ } else if (rvms[type].hasOwnProperty(t)) {
+
+ delete rulesLeft[rule.uuid];
+ } else {
+ sideData[rule.uuid][dir].missing[type][t] = 1;
+ }
+ });
+
+ for (var uuid in rulesLeft) {
+ errs.push(new verror.VError('No VMs found that match rule: %s',
+ rulesLeft[uuid].text()));
+ }
+
+ rules.forEach(function (rule) {
+ var missing = sideData[rule.uuid];
+
+ DIRECTIONS.forEach(function (dir) {
+ var otherSide = (dir == 'to' ? 'from' : 'to');
+
+ if (!missing.hasOwnProperty(dir) || objEmpty(missing[dir].vms)
+ || !missing.hasOwnProperty(otherSide)) {
+ return;
+ }
+
+ for (var type in missing[otherSide].missing) {
+ for (var t in missing[otherSide].missing[type]) {
+ errs.push(new verror.VError('Missing %s %s for rule: %s',
+ type.replace(/s$/, ''), t, rule.text()));
+ }
+ }
+ });
+ });
+
+ if (errs.length !== 0) {
+ return callback(new verror.MultiError(errs));
+ }
+
+ return callback();
+}
+
+
+/**
+ * Returns an object containing all ipf files to be written to disk, based
+ * on the given rules
+ *
+ * @param opts {Object} :
+ * - @param allVMs {Object} : VM lookup table, as returned by createVMlookup()
+ * - @param remoteVMs {Array} : array of remote VM objects (optional)
+ * - @param rules {Array} : array of rule objects
+ * - @param vms {Array} : object mapping VM UUIDs to VM objects (optional).
+ * This is used to specify VMs that might not have rules that target them,
+ * but we still want to generate conf files for. This covers cases where
+ * a rule used to target a VM, but no longer does.
+ * @param callback {Function} `function (err)`
+ */
+function prepareIPFdata(opts, callback) {
+ var allVMs = opts.allVMs;
+ var rules = opts.rules;
+ var vms = opts.vms || {};
+ var remoteVMlookup = opts.remoteVMs || { ips: {}, machines: {}, tags: {} };
+
+ var errs = [];
+ var fileData = {};
+ var ipfData = {};
+
+ if (LOG.debug()) {
+ LOG.debug({
+ rules: rules.map(function (r) { return r.uuid; }),
+ vms: Object.keys(vms)
+ // XXX remoteVMs: Object.keys(remoteVMs)
+ }, 'prepareIPFdata: entry');
+ }
+
+ var vmsLeft = Object.keys(vms).reduce(function (acc, vl) {
+ acc[vl] = 1;
+ return acc;
+ }, {});
+
+ rules.forEach(function (rule) {
+ var ips = { from: {}, to: {} };
+ var matchingVMs = { from: {}, to: {} };
+ var owner_uuid = rule.owner_uuid;
+
+ LOG.debug(rule.raw(), 'prepareIPFdata: finding matching VMs');
+
+ // XXX: don't add rule if it's disabled (but still want to find missing
+ // data for it!)
+
+ // Using the VM store, find VMs on each side
+ DIRECTIONS.forEach(function (dir) {
+ Object.keys(rule[dir]).forEach(function (type) {
+ rule[dir][type].forEach(function (t) {
+ if (!allVMs[type].hasOwnProperty(t)) {
+ LOG.debug('prepareIPFdata: dir=%s, type=%s, t=%s: not found in VMs',
+ dir, type, t);
+ return;
+ }
+
+ var matchingUUIDs = Object.keys(allVMs[type][t]);
+ LOG.debug(matchingUUIDs,
+ 'prepareIPFdata: dir=%s, type=%s, t=%s: found', dir, type, t);
+
+ matchingUUIDs.forEach(function (uuid) {
+ if (!allVMs.all.hasOwnProperty(uuid)) {
+ LOG.debug('prepareIPFdata: uuid %s not in VM store', uuid);
+ return;
+ }
+
+ if (owner_uuid && owner_uuid != allVMs.all[uuid].owner_uuid) {
+ LOG.trace('prepareIPFdata: VM %s owner_uuid=%s does not match '
+ + 'rule owner_uuid=%s for rule: %s', uuid,
+ allVMs.all[uuid].owner_uuid, owner_uuid, rule);
+ return;
+ }
+
+ matchingVMs[dir][uuid] = allVMs[type][t][uuid];
+
+ if (!noRulesNeeded(dir, rule)) {
+ delete vmsLeft[uuid];
+ }
+ });
+ });
+ });
+ });
+
+ LOG.debug(matchingVMs, 'prepareIPFdata: rule "%s" matching VMs', rule.uuid);
+
+ if (objEmpty(matchingVMs.from) && objEmpty(matchingVMs.to)) {
+ errs.push(new verror.VError(
+ 'No matching VMs found for rule: %s', rule.text()));
+ return;
+ }
+
+ // Fill out the ipfData hash: for each matching VM for a rule, we
+ // want all of the IP data from the other side of the rule (eg: for
+ // tags and machines)
+ DIRECTIONS.forEach(function (dir) {
+ var otherSide = dir === 'from' ? 'to' : 'from';
+ var missing = {};
+
+ if (noRulesNeeded(dir, rule)) {
+ LOG.trace('prepareIPFdata: rule %s (%s): ignoring side %s',
+ rule.uuid, rule.action, dir);
+ return;
+ }
+
+ // Get the tags, machines, etc. for the other side
+ Object.keys(rule[otherSide]).forEach(function (type) {
+ rule[otherSide][type].forEach(function (t) {
+ var matched = false;
+ if (type === 'ips' || type === 'subnets') {
+ // We don't need to have a VM associated with an IP or subnet:
+ // we already have all of the information needed to write a rule
+ // with it
+ return;
+ }
+
+ LOG.trace(rule[otherSide],
+ 'prepareIPFdata: rule=%s, otherSide=%s, type=%s, t=%s',
+ rule.uuid, otherSide, type, t);
+
+ if (allVMs[type].hasOwnProperty(t)) {
+ createSubObjects(ips[dir], type, t);
+ Object.keys(allVMs[type][t]).forEach(function (uuid) {
+ LOG.debug('prepareIPFdata: Adding VM "%s" (%s=%s) ips',
+ uuid, type, t);
+ allVMs.all[uuid].ips.forEach(function (ip) {
+ ips[dir][type][t][ip] = 1;
+ });
+ });
+ matched = true;
+ }
+
+ // XXX: filter by owner_uuid
+ if (remoteVMlookup[type].hasOwnProperty(t)) {
+ forEachKey(remoteVMlookup[type][t], function (uuid, rvm) {
+ createSubObjects(ips[dir], type, t);
+ rvm.ips.forEach(function (ip) {
+ ips[dir][type][t][ip] = 1;
+ });
+ });
+ matched = true;
+ }
+
+ if (!matched) {
+ createSubObjects(missing, type);
+ missing[type][t] = 1;
+ return;
+ }
+
+ });
+ });
+
+ LOG.debug(ips, 'prepareIPFdata: rule "%s" ips', rule.uuid);
+
+ if (!objEmpty(missing)) {
+ // XXX: should this maybe be a warning for some types?
+ Object.keys(missing).forEach(function (type) {
+ var items = Object.keys(missing[type]).sort();
+ errs.push(new verror.VError('rule "%s": missing %s%s: %s',
+ rule.uuid, type, items.length === 1 ? '' : 's',
+ items.join(', ')));
+ });
+ return;
+ }
+
+ Object.keys(matchingVMs[dir]).forEach(function (uuid) {
+ createSubObjects(ipfData, uuid, 'ips');
+ createSubObjects(ipfData[uuid], 'rules');
+ ipfData[uuid].rules[rule.uuid] = rule;
+ createSubObjects(ipfData[uuid], 'directions', rule.uuid);
+ ipfData[uuid].directions[rule.uuid][dir] = 1;
+
+ Object.keys(ips[dir]).forEach(function (type) {
+ createSubObjects(ipfData[uuid].ips, type);
+ Object.keys(ips[dir][type]).forEach(function (t) {
+ createSubObjects(ipfData[uuid].ips[type], t);
+ Object.keys(ips[dir][type][t]).forEach(function (ip) {
+ ipfData[uuid].ips[type][t][ip] = 1;
+ });
+ });
+ });
+ });
+
+ }); // DIRECTIONS.forEach()
+ }); // rules.forEach()
+
+ if (errs.length !== 0) {
+ return callback(new verror.MultiError(errs));
+ }
+
+ // Add any leftover VMs left in vmsLeft: these need default conf files
+ // written out for them, even if they don't have rules targeting them.
+ for (var v in vmsLeft) {
+ if (!ipfData.hasOwnProperty(v)) {
+ ipfData[v] = {};
+ }
+ }
+
+ // Finally, generate the ipf files, unless the firewall is disabled for
+ // the VM
+ var disabled = [];
+ var enabled = [];
+ for (var vm in ipfData) {
+ if (!allVMs.all[vm].enabled) {
+ disabled.push(vm);
+ continue;
+ }
+
+ enabled.push(vm);
+ var vmData = ipfFileData(vm, ipfData[vm]);
+ for (var name in vmData) {
+ var filename = util.format('%s/config/%s.conf',
+ allVMs.all[vm].zonepath, name);
+ fileData[filename] = vmData[name];
+ }
+ }
+
+ LOG.debug({ vms: enabled, disabledVMs: disabled },
+ 'prepareIPFdata: return');
+ return callback(null, { vms: enabled, files: fileData });
+}
+
+
+/*
+ * Generates ipf files for the given VM
+ */
+function ipfFileData(vmUUID, vm) {
+ var date = new Date();
+ var ipf = [
+ '# DO NOT EDIT THIS FILE. THIS FILE IS AUTO-GENERATED BY fwadm(1M)',
+ '# AND MAY BE OVERWRITTEN AT ANY TIME.',
+ '#',
+ '# File generated at ' + date.toString(),
+ '#',
+ ''];
+
+ // LOG.debug(vm, 'ipfFileData: VM "%s" ipf input data', vmUUID);
+
+ var sortBy = {};
+
+ // vm.rules might not exist
+ LOG.debug(vm.rules ? Object.keys(vm.rules) : [],
+ 'VM "%s" rules', vmUUID);
+
+ // XXX: not needed right now:
+ var toSort = {};
+
+ // Categorize rules by: ips, machines, tags
+ for (var r in vm.rules) {
+ var rule = vm.rules[r];
+ var ruleData = { uuid: rule.uuid, version: rule.version };
+
+ for (var vdir in vm.directions[r]) {
+ var oppositeSide = (vdir === 'from') ? 'to' : 'from';
+
+ for (var vtype in rule[oppositeSide]) {
+ rule[oppositeSide][vtype].forEach(function (t) {
+ var actionHash = createSubObjects(sortBy,
+ (vdir === 'from') ? 'out' : 'in',
+ vtype, t, rule.protocol, rule.action);
+
+ rule.ports.forEach(function (p) {
+ actionHash[p] = ruleData;
+ var ipfDir = (vdir === 'from') ? 'out' : 'in';
+ var key = util.format('%s/%s/%s/%s/%s/%d',
+ ipfDir, vtype, t, rule.protocol, rule.action,
+ Number(p));
+ toSort[key] = {
+ action: rule.action,
+ dir: ipfDir,
+ port: p,
+ proto: rule.protocol,
+ rule: ruleData,
+ t: t,
+ type: vtype
+ };
+ });
+ });
+ }
+ }
+ }
+
+ LOG.debug({ toSort: toSort }, 'ipfFileData: VM "%s" sorted ipf data',
+ vmUUID);
+
+ // This is super ugly, but it works. Figure out a better way to do this.
+ Object.keys(sortBy).sort().forEach(function (dir) {
+ Object.keys(sortBy[dir]).sort().forEach(function (type) {
+ Object.keys(sortBy[dir][type]).sort().forEach(function (t) {
+ Object.keys(sortBy[dir][type][t]).sort().forEach(function (proto) {
+ Object.keys(sortBy[dir][type][t][proto]).sort().forEach(
+ function (action) {
+ Object.keys(sortBy[dir][type][t][proto][action]).sort().forEach(
+ function (p) {
+ var sortedRuleData = sortBy[dir][type][t][proto][action][p];
+ var targets;
+
+ // XXX: Use pools for tags
+ if (type === 'ips' || type === 'subnets') {
+ targets = [t];
+ } else {
+ targets = Object.keys(vm.ips[type][t]);
+ }
+
+ ipf.push(util.format(
+ '# rule=%s, version=%s, %s=%s', sortedRuleData.uuid,
+ sortedRuleData.version, type.slice(0, -1), t));
+
+ targets.forEach(function (target) {
+ ipf.push(util.format(
+ '%s %s quick proto %s from %s to %s port = %d',
+ action === 'allow' ? 'pass' : 'block',
+ dir, proto,
+ dir === 'in' ? target : 'any',
+ dir === 'in' ? 'any' : target,
+ Number(p)));
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+
+ [ '',
+ '# fwadm fallbacks',
+ 'block in all',
+ 'pass out all keep state'].forEach(function (line) {
+ ipf.push(line);
+ });
+
+ return { ipf: ipf.join('\n') + '\n' };
+}
+
+
+/**
+ * Saves all of the files in ipfData to disk
+ */
+function saveIPFfiles(ipfData, callback) {
+ var ver = Date.now(0) + '.' + sprintf('%06d', process.pid);
+
+ return vasync.forEachParallel({
+ inputs: Object.keys(ipfData),
+ func: function _apply(file, cb) {
+ var tempFile = util.format('%s.%s', file, ver);
+ var oldFile = util.format('%s.old', file);
+
+ vasync.pipeline({
+ funcs: [
+ function _write(_, cb2) {
+ LOG.trace('saveIPFfiles: writing temp file "%s"', tempFile);
+ return fs.writeFile(tempFile, ipfData[file], cb2);
+ },
+ function _renameOld(_, cb2) {
+ return fs.rename(file, oldFile, function (err) {
+ if (err && err.code === 'ENOENT') {
+ return cb2(null);
+ }
+ return cb2(err);
+ });
+ },
+ function _renameTemp(_, cb2) {
+ return fs.rename(tempFile, file, cb2);
+ }
+ ]}, cb);
+ }
+ }, function (err, res) {
+ // XXX: rollback if renaming failed
+ return callback(err, res);
+ });
+}
+
+
+/**
+ * Restart the firewalls for VMs listed in uuids
+ *
+ * @param vms {Object}: VM lookup table, as returned by createVMlookup()
+ * @param rules {Array}: array of VM UUIDs to restart
+ * @param callback {Function} `function (err, restarted)`
+ * - Where restarted is a list of UUIDs for VMs that were actually restarted
+ */
+function restartFirewalls(vms, uuids, callback) {
+ LOG.trace(uuids, 'restartFirewalls: entry');
+ var restarted = [];
+
+ return vasync.forEachParallel({
+ inputs: uuids,
+ func: function _restart(uuid, cb) {
+ if (!vms.all[uuid].enabled || vms.all[uuid].state !== 'running') {
+ LOG.debug('restartFirewalls: VM "%s": not restarting '
+ + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
+ vms.all[uuid].state);
+ return cb(null);
+ }
+
+ LOG.debug('restartFirewalls: reloading firewall for VM "%s" '
+ + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
+ vms.all[uuid].state);
+
+ // Start the firewall just in case
+ return startIPF({ vm: uuid, zonepath: vms.all[uuid].zonepath },
+ function (err) {
+ restarted.push(uuid);
+ return cb(err);
+ });
+ }
+ }, function (err, res) {
+ // XXX: Does this stop on the first error?
+ return callback(err, restarted);
+ });
+}
+
+
+/**
+ * Create remote VM objects
+ *
+ * @param allVMs {Object}: VM lookup table, as returned by createVMlookup()
+ * @param vms {Array}: array of VM objects to turn into remote VMs
+ * @param callback {Function} `function (err, remoteVMs)`
+ * - Where remoteVMs is an object of remote VMs, keyed by UUID
+ */
+function createRemoteVMs(allVMs, vms, callback) {
+ LOG.trace(vms, 'createRemoteVMs: entry');
+ if (!vms || vms.length === 0) {
+ return callback();
+ }
+
+ var remoteVMs = {};
+ var errs = [];
+
+ vms.forEach(function (vm) {
+ try {
+ var rvm = util_vm.createRemoteVM(vm);
+ if (allVMs.all.hasOwnProperty(rvm.uuid)) {
+ var err = new verror.VError(
+ 'Remote VM "%s" must not have the same UUID as a local VM');
+ err.details = vm;
+ throw err;
+ }
+ remoteVMs[rvm.uuid] = rvm;
+ } catch (err2) {
+ errs.push(err2);
+ }
+ });
+
+ if (errs.length !== 0) {
+ return callback(new verror.MultiError(errs));
+ }
+
+ return callback(null, remoteVMs);
+}
+
+
+
+// --- Exported functions
+
+
+
+/**
+ * Add rules, local VMs or remote VMs
+ *
+ * @param {Object} opts : options
+ * - localVMs {Array} : list of local VMs to update
+ * - remoteVMs {Array} : list of remote VMs to add
+ * - rules {Array} : list of rules
+ * - vms {Array} : list of VMs from vmadm
+ * @param {Function} callback : of the form f(err, res)
+ */
+function add(opts, callback) {
+ try {
+ validateOpts(opts);
+ assert.optionalArrayOfObject(opts.rules, 'opts.rules');
+ assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
+ assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
+
+ var optRules = opts.rules || [];
+ var optLocalVMs = opts.localVMs || [];
+ var optRemoteVMs = opts.remoteVMs || [];
+ if (optRules.length === 0 && optLocalVMs.length === 0
+ && optRemoteVMs.length === 0) {
+ throw new Error(
+ 'Payload must contain one of: rules, localVMs, remoteVMs');
+ }
+ } catch (err) {
+ return callback(err);
+ }
+ createLogger(opts);
+ LOG.trace(opts, 'add: entry');
+
+ pipeline({
+ funcs: [
+ function rules(_, cb) { return createRules(opts.rules, cb); },
+
+ function vms(_, cb) { createVMlookup(opts.vms, cb); },
+
+ function allRules(_, cb) { loadAllRules(cb); },
+
+ // Load remote VMs from disk
+ function diskRemoteVMs(_, cb) { loadAllRemoteVMs(cb); },
+
+ function newRemoteVMs(res, cb) {
+ createRemoteVMs(res.vms, opts.remoteVMs, cb);
+ },
+
+ // Create remote VMs (if any) from payload
+ function remoteVMs(res, cb) {
+ createRemoteVMlookup(res.newRemoteVMs, cb);
+ },
+
+ // Create a combined remote VM lookup of remote VMs on disk plus
+ // new remote VMs in the payload
+ function allRemoteVMs(res, cb) {
+ createRemoteVMlookup([res.diskRemoteVMs, res.newRemoteVMs], cb);
+ },
+
+ // Get any rules that the remote VMs target
+ function remoteVMrules(res, cb) {
+ filterRulesByRemoteVMs(res.remoteVMs, res.allRules, cb);
+ },
+
+ // Get VMs the rules affect
+ function matchingVMs(res, cb) {
+ filterVMsByRules(res.vms, res.rules.concat(res.remoteVMrules), cb);
+ },
+
+ function localVMs(res, cb) {
+ lookupVMs(res.vms, opts.localVMs, cb);
+ },
+
+ function mergedVMs(res, cb) {
+ return cb(null, mergeObjects(res.matchingVMs, res.localVMs));
+ },
+
+ // Now find all rules that apply to those VMs
+ function vmRules(res, cb) {
+ filterRulesByVMs(res.vms, res.mergedVMs, res.allRules, cb);
+ },
+
+ // XXX: can probably move everything after here into its own function
+
+ // Generate the ipf files for each VM
+ function ipfData(res, cb) {
+ prepareIPFdata({
+ allVMs: res.vms,
+ remoteVMs: res.allRemoteVMs,
+ rules: res.rules.concat(res.vmRules),
+ vms: res.mergedVMs
+ }, cb);
+ },
+
+ // Save the remote VMs
+ function saveVMs(res, cb) {
+ if (opts.dryrun) {
+ return cb(null);
+ }
+ saveRemoteVMs(res.newRemoteVMs, cb);
+ },
+
+ // Save the rule files
+ function save(res, cb) {
+ if (opts.dryrun || res.rules.length === 0) {
+ return cb(null);
+ }
+ saveRules(res.rules, cb);
+ },
+ // Write the new ipf files to disk
+ function writeIPF(res, cb) {
+ if (opts.dryrun) {
+ return cb(null);
+ }
+ saveIPFfiles(res.ipfData.files, cb);
+ },
+ // Restart the firewalls for all of the affected VMs
+ function restart(res, cb) {
+ if (opts.dryrun) {
+ return cb(null);
+ }
+ restartFirewalls(res.vms, res.ipfData.vms, cb);
+ }
+ ]}, function (err, res) {
+ if (err) {
+ // XXX: log the error here
+ // if err.details, log that too
+ return callback(err);
+ }
+
+ var toReturn = {
+ vms: res.state.ipfData.vms,
+ rules: res.state.rules.map(function (r) { return r.serialize(); })
+ };
+ if (opts.filecontents) {
+ toReturn.files = res.state.ipfData.files;
+ }
+
+ return callback(err, toReturn);
+ });
+}
+