1 /*
   2  * CDDL HEADER START
   3  *
   4  * The contents of this file are subject to the terms of the
   5  * Common Development and Distribution License, Version 1.0 only
   6  * (the "License").  You may not use this file except in compliance
   7  * with the License.
   8  *
   9  * You can obtain a copy of the license at http://smartos.org/CDDL
  10  *
  11  * See the License for the specific language governing permissions
  12  * and limitations under the License.
  13  *
  14  * When distributing Covered Code, include this CDDL HEADER in each
  15  * file.
  16  *
  17  * If applicable, add the following below this CDDL HEADER, with the
  18  * fields enclosed by brackets "[]" replaced with your own identifying
  19  * information: Portions Copyright [yyyy] [name of copyright owner]
  20  *
  21  * CDDL HEADER END
  22  *
  23  *
  24  * Copyright (c) 2018, Joyent, Inc. All rights reserved.
  25  *
  26  *
  27  * fwadm: Main entry points
  28  */
  29 
  30 var assert = require('assert-plus');
  31 var clone = require('clone');
  32 var filter = require('./filter');
  33 var fs = require('fs');
  34 // XXX not ok for firewaller-agent because this won't exist on old CNs
  35 var features = require('/usr/node/node_modules/illumos-features');
  36 var mkdirp = require('mkdirp');
  37 var mod_addr = require('ip6addr');
  38 var mod_ipf = require('./ipf');
  39 var mod_lock = require('./locker');
  40 var mod_obj = require('./util/obj');
  41 var mod_rvm = require('./rvm');
  42 var mod_rule = require('fwrule');
  43 var pipeline = require('./pipeline').pipeline;
  44 var sprintf = require('extsprintf').sprintf;
  45 var util = require('util');
  46 var util_err = require('./util/errors');
  47 var util_log = require('./util/log');
  48 var util_vm = require('./util/vm');
  49 var vasync = require('vasync');
  50 var verror = require('verror');
  51 
  52 var createSubObjects = mod_obj.createSubObjects;
  53 var forEachKey = mod_obj.forEachKey;
  54 var hasKey = mod_obj.hasKey;
  55 var objEmpty = mod_obj.objEmpty;
  56 var mergeObjects = mod_obj.mergeObjects;
  57 
  58 
  59 
  60 // --- Globals
  61 
  62 
  63 
  64 var DIRECTIONS = ['from', 'to'];
  65 var RULE_PATH = '/var/fw/rules';
  66 var IPF_CONF = '%s/config/ipf.conf';
  67 var IPF_CONF_OLD = '%s/config/ipf.conf.old';
  68 var IPF6_CONF = '%s/config/ipf6.conf';
  69 var IPF6_CONF_OLD = '%s/config/ipf6.conf.old';
  70 var KEEP_FRAGS = ' keep frags';
  71 var KEEP_STATE = ' keep state';
  72 var NOT_RUNNING_MSG = 'Could not find running zone';
  73 var FEATURE_INOUT_UUID = 'com.joyent.driver.ipf.rules.in-out-uuid';
  74 // VM fields that affect filtering
  75 var VM_FIELDS = [
  76     'firewall_enabled',
  77     'nics',
  78     'owner_uuid',
  79     'state',
  80     'tags',
  81     'uuid',
  82     'zonepath'
  83 ];
  84 // VM fields required for filtering
  85 var VM_FIELDS_REQUIRED = [
  86     'nics',
  87     'state',
  88     'tags',
  89     'uuid',
  90     'zonepath'
  91 ];
  92 
  93 var icmpr = /^icmp6?$/;
  94 
  95 var fallbacks = [
  96     '',
  97     '# fwadm fallbacks',
  98     'block in all',
  99     'pass out quick proto tcp from any to any flags S/SA keep state',
 100     'pass out proto tcp from any to any',
 101     'pass out proto udp from any to any keep state'];
 102 
 103 var v4fallbacks = fallbacks.concat([
 104     'pass out quick proto icmp from any to any keep state',
 105     'pass out proto icmp from any to any']);
 106 
 107 var v6fallbacks = fallbacks.concat([
 108     'pass out quick proto ipv6-icmp from any to any keep state',
 109     'pass out proto ipv6-icmp from any to any']);
 110 
 111 
 112 // --- Internal helper functions
 113 
 114 
 115 
 116 /**
 117  * Assert that this is either a string or an object
 118  */
 119 function assertStringOrObject(obj, name) {
 120     if (typeof (obj) !== 'string' && typeof (obj) !== 'object') {
 121         assert.ok(false, name + ' ([string] or [object]) required');
 122     }
 123 }
 124 
 125 
 126 /**
 127  * For a rule and a direction, return whether or not we actually need to
 128  * write ipf rules. FROM+ALLOW and TO+BLOCK are essentially no-ops, as
 129  * they will be caught by the block / allow catch-all default rules.
 130  *
 131  * If the rule has a priority greater than 0, then we always need to write
 132  * out ipf rules.
 133  */
 134 function noRulesNeeded(dir, rule) {
 135     if (rule.priority !== 0) {
 136         return false;
 137     }
 138 
 139     if ((dir === 'from' && rule.action === 'allow')
 140         || (dir === 'to' && rule.action === 'block')) {
 141         return true;
 142     }
 143     return false;
 144 }
 145 
 146 
 147 /**
 148  * For each rule in rules, call cb for each target present in the rule,
 149  * passing the rule, direction, target type and target itself.
 150  *
 151  * @param rules {Array} : rule objects to process
 152  */
 153 function ruleTypeDirWalk(rules, cb) {
 154     rules.forEach(function (rule) {
 155         DIRECTIONS.forEach(function (dir) {
 156             ['ips', 'tags', 'vms'].forEach(function (type) {
 157                 if (hasKey(rule[dir], type)) {
 158                     rule[dir][type].forEach(function (t) {
 159                         cb(rule, dir, type, t);
 160                     });
 161                 }
 162             });
 163         });
 164     });
 165 }
 166 
 167 
 168 /**
 169  * Returns a list of rules with duplicates removed. Rules in list1 will
 170  * override rules in list2
 171  */
 172 function dedupRules(list1, list2) {
 173     var seenUUIDs = {};
 174     var toReturn = [];
 175 
 176     list1.concat(list2).forEach(function (r) {
 177         if (hasKey(r, 'uuid') && !hasKey(seenUUIDs, r.uuid)) {
 178             toReturn.push(r);
 179             seenUUIDs[r.uuid] = 1;
 180         }
 181     });
 182 
 183     return toReturn;
 184 }
 185 
 186 
 187 /**
 188  * Given a list of new rules and a map of existing rules, build a list
 189  * of the subset of new rules that are actually changing anything.
 190  */
 191 function getChangingRules(rules, existingRules, cb) {
 192     var changing = rules.filter(function (rule) {
 193         if (!hasKey(existingRules, rule.uuid)) {
 194             return true;
 195         }
 196         return !mod_obj.shallowObjEqual(rule.serialize(),
 197             existingRules[rule.uuid].serialize());
 198     });
 199 
 200     cb(null, changing);
 201 }
 202 
 203 
 204 /**
 205  * This filter removes rules that aren't affected by adding a remote VM
 206  * or updating a local VM (for example, simple wildcard rules), and would
 207  * therefore require updating the rules of other VMs. This table shows which
 208  * rules we keep. Each row represents whether or not a new VM or remote VM
 209  * is a source or destination VM, plus whether or not the rule is a BLOCK
 210  * or ALLOW rule. Each column represents a collection of targets we might
 211  * have. Each cell says whether or not we need to update the VMs represented
 212  * by that column.
 213  *
 214  *              |  From   |   To    | From |  To  | From | To
 215  *              | All VMs | All VMs | Tags | Tags | VMs  | VMs
 216  * |------------|---------|---------|------|------|------|-----
 217  * | Dest. VM / |   No    |   No    |  No  |  No  |  No  | No
 218  * |   ALLOW    |         |         |      |      |      |
 219  * |------------|---------|---------|------|------|------|-----
 220  * |  Src VM /  |   No    |   Yes   |  No  |  Yes |  No  | Yes
 221  * |   ALLOW    |         |         |      |      |      |
 222  * |------------|---------|---------|------|------|------|-----
 223  * | Dest. VM / |   Yes   |   No    |  Yes |  No  |  Yes | No
 224  * |   BLOCK    |         |         |      |      |      |
 225  * |------------|---------|---------|------|------|------|-----
 226  * |  Src VM /  |   No    |   No    |  No  |  No  |  No  | No
 227  * |   BLOCK    |         |         |      |      |      |
 228  *
 229  */
 230 function getAffectedRules(new_vms, log) {
 231     return function _isAffectedRule(rule) {
 232         if (rule.action === 'allow'
 233             && vmsOnSide(new_vms, rule, 'from', log).length > 0) {
 234             return rule.to.wildcards.indexOf('vmall') !== -1
 235                 || rule.to.tags.length > 0
 236                 || rule.to.vms.length > 0;
 237         } else if (rule.action === 'block'
 238             && vmsOnSide(new_vms, rule, 'to', log).length > 0) {
 239             return rule.from.wildcards.indexOf('vmall') !== -1
 240                 || rule.from.tags.length > 0
 241                 || rule.from.vms.length > 0;
 242         }
 243         return false;
 244     };
 245 }
 246 
 247 
 248 /**
 249  * Starts ipf and reloads the rules for a VM
 250  */
 251 function reloadIPF(opts, log, callback) {
 252     var ipfConf = util.format(IPF_CONF, opts.zonepath);
 253     var ipf6Conf = util.format(IPF6_CONF, opts.zonepath);
 254 
 255     mod_ipf.reload(opts.vm, ipfConf, ipf6Conf, log, callback);
 256 }
 257 
 258 
 259 
 260 // --- Internal functions
 261 
 262 
 263 
 264 /**
 265  * Validates the payload passed to the exported functions. Throws an error
 266  * if not in the right format
 267  */
 268 function validateOpts(opts) {
 269     assert.object(opts, 'opts');
 270     assert.ok(Array.isArray(opts.vms),
 271         'opts.vms ([object]) required');
 272     // Allow opts.vms to be empty - it's possible, though unlikely, that
 273     // there are no VMs on this system
 274     if (opts.vms.length !== 0) {
 275         assert.arrayOfObject(opts.vms, 'opts.vms');
 276     }
 277 }
 278 
 279 
 280 /**
 281  * Create rule objects from the rules
 282  *
 283  * @param {Array} inRules : raw rule input objects to create
 284  * @param {Function} callback : `f(err, newRules)`
 285  * - newRules {Array} : array of rule objects
 286  */
 287 function createRules(inRules, createdBy, callback) {
 288     var errors = [];
 289     var rules = [];
 290     var ver = mod_rule.generateVersion();
 291 
 292     if (!callback) {
 293         callback = createdBy;
 294         createdBy = null;
 295     }
 296 
 297     if (!inRules || inRules.length === 0) {
 298         return callback(null, []);
 299     }
 300 
 301     inRules.forEach(function (payloadRule) {
 302         var rule = clone(payloadRule);
 303         if (!hasKey(rule, 'version')) {
 304             rule.version = ver;
 305         }
 306 
 307         if (createdBy && !hasKey(rule, 'created_by')) {
 308             rule.created_by = createdBy;
 309         }
 310 
 311         try {
 312             var r = mod_rule.create(rule, { enforceGlobal: true });
 313             rules.push(r);
 314         } catch (err) {
 315             errors.push(err);
 316         }
 317     });
 318 
 319     if (errors.length !== 0) {
 320         return callback(util_err.createMultiError(errors));
 321     }
 322 
 323     return callback(null, rules);
 324 }
 325 
 326 
 327 /**
 328  * Merge updates from the rules in payload, and return the updated
 329  * rule objects
 330  */
 331 function createUpdatedRules(opts, log, callback) {
 332     log.trace('createUpdatedRules: entry');
 333     var originals = opts.originalRules;
 334     var updatedRules = opts.updatedRules;
 335 
 336     if (!updatedRules || updatedRules.length === 0) {
 337         return callback(null, []);
 338     }
 339 
 340     var mergedRule;
 341     var origRule;
 342     var updated = [];
 343     var ver = mod_rule.generateVersion();
 344 
 345     updatedRules.forEach(function (rule) {
 346         // Assume that we're allowed to do adds - findRules() would have errored
 347         // out if allowAdds was unset and an add was attempted
 348         if (!hasKey(rule, 'version')) {
 349             rule.version = ver;
 350         }
 351 
 352         if (opts.createdBy && !hasKey(rule, 'created_by')) {
 353             rule.created_by = opts.createdBy;
 354         }
 355 
 356         if (hasKey(originals, rule.uuid)) {
 357             origRule = originals[rule.uuid].serialize();
 358             mergedRule = mergeObjects(rule, origRule);
 359 
 360             if (!(hasKey(rule, 'owner_uuid')
 361                 && hasKey(rule, 'global'))) {
 362                 // If both owner_uuid and global are set - let
 363                 // this bubble up the appropriate error in createRules()
 364 
 365                 if (hasKey(rule, 'owner_uuid')
 366                     && hasKey(origRule, 'global')) {
 367                     // Updating from global -> owner_uuid rule
 368                     delete mergedRule.global;
 369                 }
 370 
 371                 if (hasKey(rule, 'global')
 372                     && hasKey(origRule, 'owner_uuid')) {
 373                     // Updating from owner_uuid -> global rule
 374                     delete mergedRule.owner_uuid;
 375                 }
 376             }
 377 
 378             if (mod_obj.shallowObjEqual(origRule, mergedRule)) {
 379                 // The rule hasn't changed - do nothing
 380                 delete originals[rule.uuid];
 381             } else {
 382                 updated.push(mergedRule);
 383             }
 384         } else {
 385             updated.push(rule);
 386         }
 387     });
 388 
 389     log.debug(updated, 'createUpdatedRules: rules merged');
 390     return createRules(updated, callback);
 391 }
 392 
 393 
 394 function mergeIntoLookup(vmStore, vm) {
 395     vmStore.all[vm.uuid] = vm;
 396     mod_obj.addToObj3(vmStore, 'vms', vm.uuid, vm.uuid, vm);
 397 
 398     forEachKey(vm.tags, function (tag, val) {
 399         createSubObjects(vmStore, 'tags', tag, vm.uuid, vm);
 400         createSubObjects(vmStore, 'tagValues', tag, val, vm.uuid, vm);
 401     });
 402 
 403     vm.ips.forEach(function (ip) {
 404         mod_obj.addToObj3(vmStore, 'ips', ip, vm.uuid, vm);
 405     });
 406 
 407     // XXX: subnet
 408 }
 409 
 410 
 411 /**
 412  * Turns a list of VMs from VM.js into a lookup table, keyed by the various
 413  * properties we'd like to filter VMs by (tags, ips, and vms),
 414  * like so:
 415  *   {
 416  *     all: { uuid1: <vm 1> }
 417  *     tags: { tag2: <vm 2> }
 418  *     vms: { uuid1: <vm 1> }
 419  *     ips: { 10.0.0.1: <vm 3> }
 420  *     ips: { 10.0.0.1: <vm 3> }
 421  *   }
 422  */
 423 function createVMlookup(vms, log, callback) {
 424     log.trace('createVMlookup: entry');
 425 
 426     var errs = [];
 427     var vmStore = {
 428         all: {},
 429         ips: {},
 430         subnets: {},
 431         tags: {},
 432         tagValues: {},
 433         vms: {},
 434         wildcards: {}
 435     };
 436 
 437     vmStore.wildcards.vmall = vmStore.all;
 438 
 439     vms.forEach(function (fullVM) {
 440         var missing = [];
 441         VM_FIELDS_REQUIRED.forEach(function (field) {
 442             if (!hasKey(fullVM, field)) {
 443                 missing.push(field);
 444             }
 445         });
 446 
 447         if (missing.length !== 0) {
 448             log.error({ vm: fullVM, missing: missing }, 'missing VM fields');
 449             errs.push(new verror.VError(
 450                 'VM %s: missing field%s required for firewall: %s',
 451                 fullVM.uuid,
 452                 missing.length === 0 ? '' : 's',
 453                 missing.join(', ')));
 454             return;
 455         }
 456 
 457         var vm = {
 458             enabled: fullVM.firewall_enabled || false,
 459             ips: util_vm.ipsFromNICs(fullVM.nics).sort(mod_addr.compare),
 460             owner_uuid: fullVM.owner_uuid,
 461             state: fullVM.state,
 462             tags: fullVM.tags,
 463             uuid: fullVM.uuid,
 464             zonepath: fullVM.zonepath
 465         };
 466         log.trace(vm, 'Adding VM "%s" to lookup', vm.uuid);
 467 
 468         mergeIntoLookup(vmStore, vm);
 469     });
 470 
 471     if (errs.length !== 0) {
 472         return callback(util_err.createMultiError(errs));
 473     }
 474 
 475     if (log.debug()) {
 476         var truncated = { };
 477         ['vms', 'tags', 'ips'].forEach(function (type) {
 478             truncated[type] = {};
 479             if (!hasKey(vmStore, type)) {
 480                 return;
 481             }
 482 
 483             Object.keys(vmStore[type]).forEach(function (t) {
 484                 truncated[type][t] = Object.keys(vmStore[type][t]);
 485             });
 486         });
 487 
 488         log.debug(truncated, 'vmStore');
 489     }
 490 
 491     return callback(null, vmStore);
 492 }
 493 
 494 
 495 /**
 496  * Create a lookup table for remote VMs
 497  */
 498 function createRemoteVMlookup(remoteVMs, log, callback) {
 499     return callback(null, mod_rvm.createLookup(remoteVMs, log));
 500 }
 501 
 502 
 503 /**
 504  * Load a single rule from disk, returning a rule object
 505  *
 506  * @param {String} uuid : UUID of the rule to load
 507  * @param {Function} callback : `f(err, rule)`
 508  * - vm {Object} : rule object (as per mod_rule)
 509  */
 510 function loadRule(uuid, log, callback) {
 511     var file = util.format('%s/%s.json', RULE_PATH, uuid);
 512     log.debug('loadRule: loading rule file "%s"', file);
 513 
 514     return fs.readFile(file, function (err, raw) {
 515         if (err) {
 516             if (err.code == 'ENOENT') {
 517                 var uErr = new verror.VError('Unknown rule "%s"', uuid);
 518                 uErr.code = 'ENOENT';
 519                 return callback(uErr);
 520             }
 521 
 522             return callback(err);
 523         }
 524 
 525         var rule;
 526 
 527         try {
 528             var parsed = JSON.parse(raw);
 529             log.trace(parsed, 'loadRule: loaded rule file "%s"', file);
 530             // XXX: validate that the rule has a uuid
 531             rule = mod_rule.create(parsed);
 532         } catch (err2) {
 533             log.error(err2, 'loadRule: error creating rule');
 534             return callback(err2);
 535         }
 536 
 537         if (log.trace()) {
 538             log.trace(rule.toString(), 'loadRule: created rule');
 539         }
 540 
 541         return callback(null, rule);
 542     });
 543 }
 544 
 545 
 546 /**
 547  * Loads all rules from disk
 548  */
 549 function loadAllRules(log, callback) {
 550     var rules = [];
 551 
 552     fs.readdir(RULE_PATH, function (err, files) {
 553         if (err) {
 554             if (err.code === 'ENOENT') {
 555                 return callback(null, []);
 556             }
 557             return callback(err);
 558         }
 559 
 560         return vasync.forEachParallel({
 561             inputs: files,
 562             func: function (file, cb) {
 563                 if (file.indexOf('.json', file.length - 5) === -1) {
 564                     return cb(null);
 565                 }
 566 
 567                 return loadRule(file.substring(0, file.length - 5),
 568                     log, function (err2, rule) {
 569                     if (rule) {
 570                         rules.push(rule);
 571                     }
 572                     return cb(err2);
 573                 });
 574             }
 575         }, function (err3, res) {
 576             if (err3) {
 577                 log.error(err3, 'loadAllRules: return');
 578                 return callback(err3);
 579             }
 580 
 581             log.debug({ fullRules: rules }, 'loadAllRules: return');
 582             return callback(null, rules);
 583         });
 584     });
 585 }
 586 
 587 
 588 /*
 589  * Saves rules to disk
 590  *
 591  * @param {Array} rules : rule objects to save
 592  * @param {Function} callback : `f(err)`
 593  */
 594 function saveRules(rules, log, callback) {
 595     var uuids = [];
 596     var versions = {};
 597     log.debug({ rules: rules }, 'saveRules: entry');
 598 
 599     return vasync.pipeline({
 600     funcs: [
 601         function _mkdir(_, cb) { mkdirp(RULE_PATH, cb); },
 602         function _writeRules(_, cb) {
 603             return vasync.forEachParallel({
 604                 inputs: rules,
 605                 func: function _writeRule(rule, cb2) {
 606                     var ser = rule.serialize();
 607                     // XXX: allow overriding version in the payload
 608                     var filename = util.format('%s/%s.json.%s', RULE_PATH,
 609                         rule.uuid, rule.version);
 610                     log.trace(ser, 'writing "%s"', filename);
 611 
 612                     return fs.writeFile(filename, JSON.stringify(ser, null, 2),
 613                         function (err) {
 614                         if (err) {
 615                             return cb2(err);
 616                         }
 617                         uuids.push(rule.uuid);
 618                         versions[rule.uuid] = rule.version;
 619 
 620                         return cb2(null);
 621                     });
 622                 }
 623             // XXX: if there are failures here, we want to delete these files
 624             }, cb);
 625         },
 626         function _renameRules(_, cb) {
 627             return vasync.forEachParallel({
 628                 inputs: uuids,
 629                 func: function _renameRule(uuid, cb2) {
 630                     var before = util.format('%s/%s.json.%s', RULE_PATH, uuid,
 631                         versions[uuid]);
 632                     var after = util.format('%s/%s.json', RULE_PATH, uuid);
 633                     log.trace('renaming "%s" to "%s"', before, after);
 634                     fs.rename(before, after, cb2);
 635                 }
 636             }, cb);
 637         }
 638     ]}, callback);
 639 }
 640 
 641 
 642 /*
 643  * Deletes rules on disk
 644  *
 645  * @param {Array} rules : rule objects to delete
 646  * @param {Function} callback : `f(err)`
 647  */
 648 function deleteRules(rules, log, callback) {
 649     log.debug({ rules: rules }, 'deleteRules: entry');
 650 
 651     return vasync.forEachParallel({
 652         inputs: rules.map(function (r) { return r.uuid; }),
 653         func: function _delRule(uuid, cb) {
 654             var filename = util.format('%s/%s.json', RULE_PATH, uuid);
 655             log.trace('deleting "%s"', filename);
 656 
 657             fs.unlink(filename, function (err) {
 658                 if (err && err.code == 'ENOENT') {
 659                     return cb();
 660                 }
 661 
 662                 return cb(err);
 663             });
 664         }
 665     }, callback);
 666 }
 667 
 668 
 669 /**
 670  * Loads rules and remote VMs from disk
 671  */
 672 function loadDataFromDisk(log, callback) {
 673     var onDisk = {};
 674 
 675     vasync.parallel({
 676         funcs: [
 677             function _diskRules(cb) {
 678                 loadAllRules(log, function (err, res) {
 679                     if (res) {
 680                         onDisk.rules = res;
 681                         onDisk.rulesByUUID = {};
 682                         onDisk.rules.forEach(function (rule) {
 683                             onDisk.rulesByUUID[rule.uuid] = rule;
 684                         });
 685                     }
 686 
 687                     return cb(err);
 688                 });
 689             },
 690 
 691             function _diskRemoteVMs(cb) {
 692                 mod_rvm.loadAll(log, function (err, res) {
 693                     if (res) {
 694                         onDisk.remoteVMs = res;
 695                     }
 696 
 697                     return cb(err);
 698                 });
 699             }
 700         ]
 701     }, function (err) {
 702         if (err) {
 703             return callback(err);
 704         }
 705 
 706         return callback(null, onDisk);
 707     });
 708 }
 709 
 710 
 711 /**
 712  * Finds rules in the list, returning an error if they can't be found
 713  */
 714 function findRules(opts, log, callback) {
 715     log.trace('findRules: entry');
 716     var allowAdds = opts.allowAdds || false;
 717     var allRules = opts.allRules;
 718     var rules = opts.rules;
 719 
 720     if (!rules || rules.length === 0) {
 721         return callback(null, []);
 722     }
 723 
 724     var errs = [];
 725     var found = {};
 726     var missing = {};
 727 
 728     rules.forEach(function (r) {
 729         if (!hasKey(r, 'uuid')) {
 730             errs.push(new verror.VError('Missing UUID of rule: %j', r));
 731             return;
 732         }
 733 
 734         if (hasKey(allRules, r.uuid)) {
 735             found[r.uuid] = allRules[r.uuid];
 736         } else {
 737             missing[r.uuid] = 1;
 738         }
 739     });
 740 
 741     // If we're allowing adds, missing rules aren't an error
 742     if (!allowAdds && !objEmpty(missing)) {
 743         Object.keys(missing).forEach(function (uuid) {
 744             errs.push(new verror.VError('Unknown rule: %s', uuid));
 745         });
 746     }
 747 
 748     if (log.debug()) {
 749         var ret = { rules: found };
 750         if (allowAdds) {
 751             ret.adds = Object.keys(missing);
 752         } else {
 753             ret.missing = Object.keys(missing);
 754         }
 755         log.debug(ret, 'findRules: return');
 756     }
 757 
 758     if (errs.length !== 0) {
 759         callback(util_err.createMultiError(errs));
 760         return;
 761     }
 762 
 763     callback(null, found);
 764 }
 765 
 766 
 767 /**
 768  * Looks up the given VMs in the VM lookup object, and returns an
 769  * object mapping UUIDs to VM lookup objects
 770  */
 771 function lookupVMs(allVMs, vms, log, callback) {
 772     log.debug({ vms: vms }, 'lookupVMs: entry');
 773 
 774     if (!vms || vms.length === 0) {
 775         log.debug('lookupVMs: no VMs to lookup: returning');
 776         return callback(null, {});
 777     }
 778 
 779     var toReturn = {};
 780     var errs = [];
 781     vms.forEach(function (vm) {
 782         if (!hasKey(vm, 'uuid')) {
 783             errs.push(new verror.VError('VM missing uuid property: %j', vm));
 784             return;
 785         }
 786         if (!hasKey(allVMs.all, vm.uuid)) {
 787             errs.push(new verror.VError('Could not find VM "%s" in VM list',
 788                 vm.uuid));
 789             return;
 790         }
 791         toReturn[vm.uuid] = allVMs.all[vm.uuid];
 792     });
 793 
 794     if (errs.length !== 0) {
 795         return callback(util_err.createMultiError(errs));
 796     }
 797 
 798     log.debug({ vms: toReturn }, 'lookupVMs: return');
 799     return callback(null, toReturn);
 800 }
 801 
 802 
 803 /**
 804  * Validates the list of rules, ensuring that there's enough information
 805  * to write each rule to disk
 806  *
 807  * @param vms {Object}: VM lookup table, as returned by createVMlookup()
 808  * @param rvms {Object}: remote VM lookup table, as returned by
 809  *     createRemoteVMlookup()
 810  * @param rules {Array}: array of rule objects
 811  * @param callback {Function} `function (err)`
 812  */
 813 function validateRules(vms, rvms, rules, log, callback) {
 814     log.trace(rules, 'validateRules: entry');
 815     var sideData = {};
 816     var errs = [];
 817     var rulesLeft = rules.reduce(function (h, r) {
 818         h[r.uuid] = r;
 819         return h;
 820     }, {});
 821 
 822     // XXX: make owner uuid aware
 823 
 824     // First go through the rules finding all the VMs we need rules for,
 825     // and mark any missing types
 826     ruleTypeDirWalk(rules, function _getRuleData(rule, dir, type, t) {
 827         // Don't bother checking IPs, since we don't need any additional
 828         // data in order to create an ipf rule
 829         if (type == 'ips') {
 830             return;
 831         }
 832 
 833         // Allow creating rules that target tags, but not any specific VMs
 834         if (type == 'tags') {
 835             delete rulesLeft[rule.uuid];
 836             return;
 837         }
 838 
 839         createSubObjects(sideData, rule.uuid, dir, 'missing', type);
 840         createSubObjects(sideData, rule.uuid, dir, 'vms');
 841 
 842         if (hasKey(vms[type], t)) {
 843             for (var vm in vms[type][t]) {
 844                 sideData[rule.uuid][dir].vms[vm] = 1;
 845             }
 846             delete rulesLeft[rule.uuid];
 847 
 848         } else if (hasKey(rvms[type], t)) {
 849             delete rulesLeft[rule.uuid];
 850 
 851         } else {
 852             sideData[rule.uuid][dir].missing[type][t] = 1;
 853         }
 854     });
 855 
 856     for (var uuid in rulesLeft) {
 857         errs.push(new verror.VError('No VMs found that match rule: %s',
 858                 rulesLeft[uuid].text()));
 859     }
 860 
 861     rules.forEach(function (rule) {
 862         var missing = sideData[rule.uuid];
 863 
 864         if (!missing) {
 865             return;
 866         }
 867 
 868         DIRECTIONS.forEach(function (dir) {
 869             var otherSide = (dir == 'to' ? 'from' : 'to');
 870 
 871             if (!hasKey(missing, dir) || objEmpty(missing[dir].vms)
 872                 || !hasKey(missing, otherSide)) {
 873                 return;
 874             }
 875 
 876             for (var type in missing[otherSide].missing) {
 877                 for (var t in missing[otherSide].missing[type]) {
 878                     errs.push(new verror.VError('Missing %s %s for rule: %s',
 879                             type.replace(/s$/, ''), t, rule.text()));
 880                 }
 881             }
 882         });
 883     });
 884 
 885     if (errs.length !== 0) {
 886         return callback(util_err.createMultiError(errs));
 887     }
 888 
 889     return callback();
 890 }
 891 
 892 
 893 /**
 894  * Returns the appropriate target string based on the rule's protocol
 895  * (eg: code for ICMP, port for TCP / UDP)
 896  */
 897 function protoTarget(rule, target) {
 898     if (target === 'all') {
 899         return '';
 900     } else if (icmpr.test(rule.protocol)) {
 901         var typeArr = target.split(':');
 902         return 'icmp-type ' + typeArr[0]
 903             + (typeArr.length === 1 ? '' : ' code ' + typeArr[1]);
 904     } else {
 905         if (hasKey(target, 'start')
 906             && hasKey(target, 'end')) {
 907 
 908             return 'port ' + target.start + ' : ' + target.end;
 909         } else {
 910             return 'port = ' + target;
 911         }
 912     }
 913 }
 914 
 915 
 916 /**
 917  * Compare two port targets. Valid values are "all", numbers, or an object
 918  * representing a port range containing "start" and "end" fields.
 919  */
 920 function comparePorts(p1, p2) {
 921     // "all" comes before any port numbers
 922     if (p1 === 'all') {
 923         if (p2 === 'all') {
 924             return 0;
 925         } else {
 926             return -1;
 927         }
 928     } else if (p2 === 'all') {
 929         return 1;
 930     }
 931 
 932     var n1 = p1.hasOwnProperty('start') ? p1.start : p1;
 933     var n2 = p2.hasOwnProperty('start') ? p2.start : p2;
 934 
 935     return Number(n1) - Number(n2);
 936 }
 937 
 938 
 939 /**
 940  * Compare two ICMP type targets. Valid values are "all" or strings like "5" or
 941  * "5:3", representing the ICMP type number and code.
 942  */
 943 function compareTypes(t1, t2) {
 944     // "all" comes before any types
 945     if (t1 === 'all') {
 946         if (t2 === 'all') {
 947             return 0;
 948         } else {
 949             return -1;
 950         }
 951     } else if (t2 === 'all') {
 952         return 1;
 953     }
 954 
 955     var p1 = t1.split(':');
 956     var p2 = t2.split(':');
 957     var c = Number(p1[0]) - Number(p2[0]);
 958     if (c !== 0) {
 959         return c;
 960     }
 961 
 962     if (p1.length === 1) {
 963         if (p2.length === 1) {
 964             return 0;
 965         } else {
 966             return -1;
 967         }
 968     } else if (p2.length === 1) {
 969         return 1;
 970     } else {
 971         return Number(p1[1]) - Number(p2[1]);
 972     }
 973 }
 974 
 975 
 976 /**
 977  * Compare IP and subnet targets from an ipf rule object.
 978  */
 979 function compareAddrs(a1, a2) {
 980     var s1 = a1.split('/');
 981     var s2 = a2.split('/');
 982 
 983     return mod_addr.compare(s1[0], s2[0]);
 984 }
 985 
 986 
 987 /**
 988  * This comparison function is used to sort the output ipf rule objects
 989  * in the following order:
 990  *
 991  * 1. First by protocol: icmp, icmp6, tcp, udp.
 992  * 2. Then by direction: outbound rules, then inbound.
 993  * 3. By priority level (higher priorities come first).
 994  * 4. By action:
 995  *   a) For outbound traffic, block comes before allow.
 996  *   b) For inbound traffic, allow comes before block.
 997  * 5. By the first port or type listed.
 998  * 6. By the first targeted IP.
 999  *
1000  * 1 and 2 help apply some organization to the output file.
1001  * 5 and 6 help make testing easier by putting things in a predictable order.
1002  *
1003  * 3 and 4 are the actual, important metric to sort on: priority and action
1004  * are important for ensuring that the actions taken by ipfilter are applied in
1005  * the order that fwadm(1M) describes.
1006  */
1007 function compareRules(r1, r2) {
1008     var res;
1009 
1010     // Protocol:
1011     if (r1.protocol < r2.protocol) {
1012         return -1;
1013     }
1014 
1015     if (r1.protocol > r2.protocol) {
1016         return 1;
1017     }
1018 
1019     // Direction:
1020     if (r1.direction < r2.direction) {
1021         return -1;
1022     }
1023 
1024     if (r1.direction > r2.direction) {
1025         return 1;
1026     }
1027 
1028     // Priority levels:
1029     if (r1.priority < r2.priority) {
1030         return 1;
1031     }
1032 
1033     if (r1.priority > r2.priority) {
1034         return -1;
1035     }
1036 
1037     // Action:
1038     if (r1.direction === 'from') {
1039         if (r1.action === 'allow') {
1040             if (r2.action === 'block') {
1041                 return 1;
1042             }
1043         } else if (r2.action === 'allow') {
1044             if (r1.action === 'block') {
1045                 return -1;
1046             }
1047         }
1048     } else {
1049         if (r1.action === 'allow') {
1050             if (r2.action === 'block') {
1051                 return -1;
1052             }
1053         } else if (r2.action === 'allow') {
1054             if (r1.action === 'block') {
1055                 return 1;
1056             }
1057         }
1058     }
1059 
1060     // Ports and types:
1061     if (icmpr.test(r1.protocol)) {
1062         res = compareTypes(r1.protoTargets[0], r2.protoTargets[0]);
1063     } else {
1064         res = comparePorts(r1.protoTargets[0], r2.protoTargets[0]);
1065     }
1066 
1067     if (res !== 0) {
1068         return res;
1069     }
1070 
1071     // Target IPs and subnets:
1072     return compareAddrs(r1.targets[0], r2.targets[0]);
1073 }
1074 
1075 
1076 /**
1077  * Returns an object containing ipf rule text and enough data to sort on
1078  */
1079 function ipfRuleObj(opts) {
1080     var dir = opts.direction;
1081     var rule = opts.rule;
1082 
1083     var targets = Array.isArray(opts.targets) ?
1084         opts.targets : [ opts.targets ];
1085 
1086     // ipfilter uses /etc/protocols which calls ICMPv6 'ipv6-icmp'
1087     var ipfProto = (rule.protocol === 'icmp6') ? 'ipv6-icmp' : rule.protocol;
1088 
1089     var readtags = [];
1090     if (features.feature[FEATURE_INOUT_UUID]) {
1091         if (rule.uuid) {
1092             readtags.push(util.format('uuid=%s', rule.uuid));
1093         }
1094         if (rule.log) {
1095             readtags.push('cfwlog');
1096         }
1097     }
1098 
1099     var sortObj = {
1100         action: rule.action,
1101         direction: dir,
1102         priority: rule.priority,
1103         protocol: rule.protocol,
1104         header: util.format('\n# rule=%s, version=%s, %s=%s',
1105             rule.uuid, rule.version, opts.type, opts.value),
1106         v4text: [],
1107         v6text: [],
1108         targets: targets,
1109         protoTargets: rule.protoTargets,
1110         type: opts.type,
1111         uuid: rule.uuid,
1112         value: opts.value,
1113         version: rule.version,
1114         allTags: readtags.length !== 0 ?
1115             util.format(' set-tag(%s)', readtags.join(', ')) : ''
1116     };
1117 
1118     if (opts.type === 'wildcard' && opts.value === 'any') {
1119         rule.protoTargets.forEach(function (t) {
1120             var wild = util.format('%s %s quick proto %s from any to any %s',
1121                 rule.action === 'allow' ? 'pass' : 'block',
1122                 dir === 'from' ? 'out' : 'in',
1123                 ipfProto,
1124                 protoTarget(rule, t));
1125             if (rule.protocol !== 'icmp6')
1126                 sortObj.v4text.push(wild);
1127             if (rule.protocol !== 'icmp')
1128                 sortObj.v6text.push(wild);
1129         });
1130 
1131         return sortObj;
1132     }
1133 
1134     targets.forEach(function (target) {
1135         var isv6 = target.indexOf(':') !== -1;
1136 
1137         // Don't generate rules for ICMPv4/IPv6 or ICMPv6/IPv4
1138         if ((isv6 && rule.protocol === 'icmp')
1139             || (!isv6 && rule.protocol === 'icmp6')) {
1140             return;
1141         }
1142 
1143         var text = isv6 ? sortObj.v6text : sortObj.v4text;
1144 
1145         rule.protoTargets.forEach(function (t) {
1146             text.push(
1147                 util.format('%s %s quick proto %s from %s to %s %s',
1148                     rule.action === 'allow' ? 'pass' : 'block',
1149                     dir === 'from' ? 'out' : 'in',
1150                     ipfProto,
1151                     dir === 'to' ? target : 'any',
1152                     dir === 'to' ? 'any' : target,
1153                     protoTarget(rule, t)))
1154         });
1155     });
1156 
1157     return sortObj;
1158 }
1159 
1160 
1161 /**
1162  * Returns an object containing all ipf files to be written to disk, based
1163  * on the given rules
1164  *
1165  * @param opts {Object} :
1166  * - @param allVMs {Object} : VM lookup table, as returned by createVMlookup()
1167  * - @param remoteVMs {Array} : array of remote VM objects (optional)
1168  * - @param rules {Array} : array of rule objects
1169  * - @param vms {Array} : object mapping VM UUIDs to VM objects. All VMs in
1170  *   this object will have conf files written. This covers the case where
1171  *   a rule used to target a VM, but no longer does, so we want to write the
1172  *   config minus the rule that no longer applies.
1173  * @param callback {Function} `function (err)`
1174  */
1175 function prepareIPFdata(opts, log, callback) {
1176     var allVMs = opts.allVMs;
1177     var date = new Date();
1178     var rules = opts.rules;
1179     var vms = opts.vms;
1180     var remoteVMs = opts.remoteVMs || { ips: {}, vms: {}, tags: {} };
1181 
1182     log.debug({ vms: vms, rules: rules }, 'prepareIPFdata: entry');
1183 
1184     var conf = {};
1185     if (vms) {
1186         conf = Object.keys(vms).reduce(function (acc, v) {
1187             // If the VM's firewall is disabled, we don't need to write out
1188             // rules for it
1189             if (allVMs.all[v].enabled) {
1190                 acc[v] = [];
1191             }
1192             return acc;
1193         }, {});
1194     }
1195 
1196     /* Gather the VMs targeted on each side of every enabled rule. */
1197     var targetVMs = rules.map(function (rule) {
1198         if (!rule.enabled) {
1199             return null;
1200         }
1201 
1202         return {
1203             from: vmsOnSide(allVMs, rule, 'from', log),
1204             to: vmsOnSide(allVMs, rule, 'to', log)
1205         };
1206     });
1207 
1208     /*
1209      * If we block outbound traffic for a protocol, make sure to also track
1210      * inbound state for anything allowed, so that we'll allow response
1211      * packets.
1212      *
1213      * We could just always enable state tracking for all of our inbound
1214      * allow rules, but state tracking can get pretty expensive. There's no
1215      * need to penalize all firewall users.
1216      */
1217     var keepInboundState = { };
1218     rules.forEach(function (rule, i) {
1219         if (!rule.enabled || rule.action === 'allow') {
1220             return;
1221         }
1222 
1223         targetVMs[i].from.forEach(function (uuid) {
1224             if (!hasKey(keepInboundState, uuid)) {
1225                 keepInboundState[uuid] = {};
1226             }
1227             keepInboundState[uuid][rule.protocol] = true;
1228         });
1229     });
1230 
1231     rules.forEach(function (rule, i) {
1232         if (!rule.enabled) {
1233             return;
1234         }
1235 
1236         DIRECTIONS.forEach(function (dir) {
1237             // XXX: add to errors here if missing
1238 
1239             // Default outgoing policy is 'allow' and default incoming policy
1240             // is 'block', so these are effectively no-ops:
1241             if (noRulesNeeded(dir, rule)) {
1242                 return;
1243             }
1244 
1245             var otherSideRules =
1246                 rulesFromOtherSide(rule, dir, allVMs, remoteVMs);
1247 
1248             targetVMs[i][dir].forEach(function (uuid) {
1249                 /*
1250                  * If the VM's firewall is disabled, we don't need to write out
1251                  * rules for it.
1252                  */
1253                 if (!allVMs.all[uuid].enabled || !hasKey(conf, uuid)) {
1254                     return;
1255                 }
1256 
1257                 conf[uuid] = conf[uuid].concat(otherSideRules);
1258             });
1259         });
1260     });
1261 
1262     var toReturn = [];
1263     for (var vm in conf) {
1264         var rulesIncluded = {};
1265         var ipf4Conf = [
1266             '# DO NOT EDIT THIS FILE. THIS FILE IS AUTO-GENERATED BY fwadm(1M)',
1267             '# AND MAY BE OVERWRITTEN AT ANY TIME.',
1268             '#',
1269             '# File generated at ' + date.toString(),
1270             '#',
1271             ''];
1272         var ipf6Conf = ipf4Conf.slice();
1273         var iks = hasKey(keepInboundState, vm) ? keepInboundState[vm] : {};
1274 
1275         conf[vm].sort(compareRules).forEach(function (sortObj) {
1276             assert.string(sortObj.allTags, 'sortObj.allTags');
1277             var ktxt = KEEP_FRAGS;
1278             if (sortObj.allTags !== ''
1279                 || (sortObj.direction === 'from' && sortObj.action === 'allow')
1280                 || (sortObj.direction === 'to' && iks[sortObj.protocol])) {
1281                 ktxt += KEEP_STATE + sortObj.allTags;
1282             }
1283 
1284             if (!hasKey(rulesIncluded, sortObj.uuid)) {
1285                 rulesIncluded[sortObj.uuid] = [];
1286             }
1287             rulesIncluded[sortObj.uuid].push(sortObj.direction);
1288 
1289             ipf4Conf.push(sortObj.header);
1290             ipf6Conf.push(sortObj.header);
1291 
1292             sortObj.v4text.forEach(function (line) {
1293                 ipf4Conf.push(line + ktxt);
1294             });
1295             sortObj.v6text.forEach(function (line) {
1296                 ipf6Conf.push(line + ktxt);
1297             });
1298         });
1299 
1300         log.debug(rulesIncluded, 'VM %s: generated ipf(6).conf', vm);
1301 
1302         var v4rules = ipf4Conf.concat(v4fallbacks);
1303         var v6rules = ipf6Conf.concat(v6fallbacks);
1304 
1305         toReturn.push({
1306             uuid: vm,
1307             zonepath: allVMs.all[vm].zonepath,
1308             v4text: v4rules.join('\n') + '\n',
1309             v6text: v6rules.join('\n') + '\n'
1310         });
1311     }
1312 
1313     return callback(null, toReturn);
1314 }
1315 
1316 
1317 /**
1318  * Returns an array of the UUIDs of VMs on the given side of a rule
1319  */
1320 function vmsOnSide(allVMs, rule, dir, log) {
1321     var matching = [];
1322 
1323     ['vms', 'tags', 'wildcards'].forEach(function (walkType) {
1324         rule[dir][walkType].forEach(function (t) {
1325             var type = walkType;
1326             var value;
1327             if (typeof (t) !== 'string') {
1328                 value = t[1];
1329                 t = t[0];
1330                 type = 'tagValues';
1331             }
1332 
1333             if (type === 'wildcards' && t === 'any') {
1334                 return;
1335             }
1336 
1337             if (!allVMs[type] || !hasKey(allVMs[type], t)) {
1338                 log.debug('No matching VMs found in lookup for %s=%s', type, t);
1339                 return;
1340             }
1341 
1342             var vmList = allVMs[type][t];
1343             if (value !== undefined) {
1344                 if (!hasKey(vmList, value)) {
1345                     return;
1346                 }
1347                 vmList = vmList[value];
1348             }
1349 
1350             Object.keys(vmList).forEach(function (uuid) {
1351                 if (hasKey(rule, 'owner_uuid')
1352                     && (rule.owner_uuid != vmList[uuid].owner_uuid)) {
1353                     return;
1354                 }
1355 
1356                 matching.push(uuid);
1357             });
1358         });
1359     });
1360 
1361     return matching;
1362 }
1363 
1364 
1365 /**
1366  * Returns the ipf rules for the opposite side of a rule
1367  */
1368 function rulesFromOtherSide(rule, dir, localVMs, remoteVMs) {
1369     var otherSide = dir === 'from' ? 'to' : 'from';
1370     var ipfRules = [];
1371 
1372     if (rule[otherSide].wildcards.indexOf('any') !== -1) {
1373         ipfRules.push(ipfRuleObj({
1374             rule: rule,
1375             direction: dir,
1376             targets: [ '0.0.0.0' ],
1377             type: 'wildcard',
1378             value: 'any'
1379         }));
1380 
1381         return ipfRules;
1382     }
1383 
1384     // IPs and subnets don't need looking up in the local or remote VM
1385     // lookup objects, so just them as-is
1386     ['ip', 'subnet'].forEach(function (type) {
1387         rule[otherSide][type + 's'].forEach(function (value) {
1388             ipfRules.push(ipfRuleObj({
1389                 rule: rule,
1390                 direction: dir,
1391                 targets: value,
1392                 type: type,
1393                 value: value
1394             }));
1395         });
1396     });
1397 
1398     // Lookup the VMs in the local and remove VM lookups, and add their IPs
1399     // accordingly
1400     ['tag', 'vm', 'wildcard'].forEach(function (type) {
1401         var typePlural = type + 's';
1402         rule[otherSide][typePlural].forEach(function (value) {
1403             var lookupType = type;
1404             var lookupTypePlural = typePlural;
1405             var t;
1406 
1407             if (typeof (value) !== 'string') {
1408                 t = value[1];
1409                 value = value[0];
1410                 lookupType = 'tagValue';
1411                 lookupTypePlural = 'tagValues';
1412             }
1413 
1414             if (lookupTypePlural === 'wildcards' && value === 'any') {
1415                 return;
1416             }
1417 
1418             [localVMs, remoteVMs].forEach(function (lookup) {
1419                 if (!hasKey(lookup, lookupTypePlural)
1420                     || !hasKey(lookup[lookupTypePlural], value)) {
1421                     return;
1422                 }
1423 
1424                 var vmList = lookup[lookupTypePlural][value];
1425                 if (t !== undefined) {
1426                     if (!hasKey(vmList, t)) {
1427                         return;
1428                     }
1429                     vmList = vmList[t];
1430                 }
1431 
1432                 forEachKey(vmList, function (uuid, vm) {
1433                     if (rule.owner_uuid && vm.owner_uuid
1434                         && vm.owner_uuid != rule.owner_uuid) {
1435                         return;
1436                     }
1437 
1438                     if (vm.ips.length === 0) {
1439                         return;
1440                     }
1441 
1442                     ipfRules.push(ipfRuleObj({
1443                         rule: rule,
1444                         direction: dir,
1445                         targets: vm.ips,
1446                         type: lookupType,
1447                         value: value
1448                     }));
1449                 });
1450             });
1451         });
1452     });
1453 
1454     return ipfRules;
1455 }
1456 
1457 
1458 /**
1459  * Gets remote targets from the other side of the rule and adds them to
1460  * the targets object
1461  */
1462 function addOtherSideRemoteTargets(vms, rule, targets, dir, log) {
1463     var matching = vmsOnSide(vms, rule, dir, log);
1464     if (matching.length === 0) {
1465         return;
1466     }
1467 
1468     var otherSide = dir === 'from' ? 'to' : 'from';
1469     if (rule[otherSide].tags.length !== 0) {
1470         if (!hasKey(targets, 'tags')) {
1471             targets.tags = {};
1472         }
1473 
1474         // All tags (no value) wins out over tags with
1475         // a value. If multiple values for the same tag
1476         // are present, return them as an array
1477         rule[otherSide].tags.forEach(function (tag) {
1478             var key = tag;
1479             var val = true;
1480             if (typeof (tag) !== 'string') {
1481                 key = tag[0];
1482                 val = tag[1];
1483             }
1484 
1485             if (!hasKey(targets.tags, key)) {
1486                 targets.tags[key] = val;
1487             } else {
1488                 if (targets.tags[key] !== true) {
1489                     if (val === true) {
1490                         targets.tags[key] = val;
1491                     } else {
1492                         if (!Array.isArray(targets.tags[key])) {
1493                             targets.tags[key] = [ targets.tags[key] ];
1494                         }
1495 
1496                         if (targets.tags[key].indexOf(val) === -1) {
1497                             targets.tags[key].push(val);
1498                         }
1499                     }
1500                 }
1501             }
1502         });
1503     }
1504 
1505     if (rule[otherSide].vms.length !== 0) {
1506         if (!hasKey(targets, 'vms')) {
1507             targets.vms = {};
1508         }
1509 
1510         rule[otherSide].vms.forEach(function (vm) {
1511             // Don't add if it's a local VM
1512             if (!hasKey(vms.all, vm)) {
1513                 targets.vms[vm] = true;
1514             }
1515         });
1516     }
1517 
1518     if (rule[otherSide].wildcards.indexOf('vmall') !== -1) {
1519         targets.allVMs = true;
1520     }
1521 }
1522 
1523 
1524 /**
1525  * Carefully move the new ipfilter configuration file into place. It's
1526  * important to make sure that if we fail or crash at any point that we
1527  * leave a file in place, since its presence is what determines whether
1528  * to enable the firewall at zone boot.
1529  */
1530 function replaceIPFconf(file, data, ver, callback) {
1531     var tempFile = util.format('%s.%s', file, ver);
1532     var oldFile = util.format('%s.old', file);
1533 
1534     vasync.pipeline({
1535     funcs: [
1536         function _write(_, cb) {
1537             fs.writeFile(tempFile, data, cb);
1538         },
1539         function _unlinkOld(_, cb) {
1540             fs.unlink(oldFile, function (err) {
1541                 if (err && err.code === 'ENOENT') {
1542                     cb(null);
1543                     return;
1544                 }
1545 
1546                 cb(err);
1547             });
1548         },
1549         function _linkOld(_, cb) {
1550             fs.link(file, oldFile, function (err) {
1551                 if (err && err.code === 'ENOENT') {
1552                     cb(null);
1553                     return;
1554                 }
1555 
1556                 cb(err);
1557             });
1558         },
1559         function _renameTemp(_, cb) {
1560             fs.rename(tempFile, file, cb);
1561         }
1562     ]}, callback);
1563 }
1564 
1565 
1566 /**
1567  * Saves all of the generated ipfilter rules in ipfData to disk. We handle
1568  * each VM separately in parallel, so that failures for one don't impact
1569  * reloading others. For example, a VM may have filled up its disk, and we
1570  * now can't write out its configuration, or a VM may have stopped on us
1571  * before we had a chance to run ipf(1M) on it.
1572  */
1573 function saveConfsAndReload(opts, ipfData, log, callback) {
1574     var ver = Date.now(0) + '.' + sprintf('%06d', process.pid);
1575     var files = {};
1576     var uuids = [];
1577 
1578     vasync.forEachParallel({
1579         inputs: ipfData,
1580         func: function (vm, cb) {
1581             uuids.push(vm.uuid);
1582 
1583             vasync.pipeline({
1584             funcs: [
1585                 // Write the new ipf.conf for IPv4 rules:
1586                 function writeV4(_, cb2) {
1587                     var filename = util.format(IPF_CONF, vm.zonepath);
1588                     files[filename] = vm.v4text;
1589 
1590                     if (opts.dryrun) {
1591                         cb2(null);
1592                         return;
1593                     }
1594 
1595                     replaceIPFconf(filename, vm.v4text, ver, cb2);
1596                 },
1597 
1598                 // Write the new ipf6.conf for IPv6 rules:
1599                 function writeV6(_, cb2) {
1600                     var filename = util.format(IPF6_CONF, vm.zonepath);
1601                     files[filename] = vm.v6text;
1602 
1603                     if (opts.dryrun) {
1604                         cb2(null);
1605                         return;
1606                     }
1607 
1608                     replaceIPFconf(filename, vm.v6text, ver, cb2);
1609                 },
1610 
1611                 // Restart the VM's firewall:
1612                 function reload(_, cb2) {
1613                     if (opts.dryrun) {
1614                         cb2(null);
1615                         return;
1616                     }
1617 
1618                     restartFirewall(opts.allVMs, vm.uuid, log, cb2);
1619                 }
1620             ]}, cb);
1621         }
1622     }, function (err) {
1623         if (err) {
1624             callback(err);
1625             return;
1626         }
1627 
1628         callback(null, {
1629             files: files,
1630             vms: uuids
1631         });
1632     });
1633 }
1634 
1635 
1636 /**
1637  * Restart the given VMs firewall.
1638  *
1639  * @param vms {Object}: VM lookup table, as returned by createVMlookup()
1640  * @param uuid {UUID}: The UUID of the target VM
1641  * @param callback {Function} `function (err)`
1642  */
1643 function restartFirewall(vms, uuid, log, cb) {
1644     if (!vms.all[uuid].enabled || vms.all[uuid].state !== 'running') {
1645         log.debug('restartFirewalls: VM "%s": not restarting '
1646             + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
1647             vms.all[uuid].state);
1648         cb(null);
1649         return;
1650     }
1651 
1652     log.debug('restartFirewalls: reloading firewall for VM "%s" '
1653         + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
1654         vms.all[uuid].state);
1655 
1656     // Reload the firewall, and start it if necessary.
1657     reloadIPF({ vm: uuid, zonepath: vms.all[uuid].zonepath }, log,
1658         function (err, res) {
1659         if (err && zoneNotRunning(res)) {
1660             /*
1661              * An error starting the firewall due to the zone not
1662              * running isn't really an error.
1663              */
1664             cb();
1665             return;
1666         }
1667 
1668         cb(err);
1669     });
1670 }
1671 
1672 
1673 /**
1674  * Applies firewall changes:
1675  * - saves / deletes rule files as needed
1676  * - writes out ipf conf files
1677  * - starts or restarts ipf in VMs
1678  *
1679  * @param {Object} opts :
1680  *   - allRemoteVMs {Object} : VM lookup object of all remote VMs
1681  *   - allVMs {Object} : VM lookup object of all local VMs
1682  *   - del {Object} : Objects to delete from disk:
1683  *     - rules {Array of Objects} : rules objects to delete
1684  *     - rvms {Array of Objects} : remote VM UUIDs to delete
1685  *   - dryRun {Bool} : if true, no files will be written or firewalls reloaded
1686  *   - rules {Array of Objects} : rules to write out
1687  *   - save {Object} : Objects to save to disk:
1688  *     - rules {Array of Objects} : rule objects to save
1689  *     - remoteVMs {Array of Objects} : remote VM objects to save
1690  *   - vms {Object} : Mapping of UUID to VM object - VMs to write out
1691  *     firewalls for, regardless of whether or not rules affect them
1692  *     (necessary for catching the case where a VM used to have rules that
1693  *     applied to it but no longer does)
1694  */
1695 function applyChanges(opts, log, callback) {
1696     log.trace(opts, 'applyChanges: entry');
1697 
1698     assert.object(opts, 'opts');
1699     assert.optionalObject(opts.allRemoteVMs, 'opts.allRemoteVMs');
1700     assert.optionalObject(opts.allVMs, 'opts.allVMs');
1701     assert.optionalObject(opts.del, 'opts.del');
1702     assert.optionalArrayOfObject(opts.rules, 'opts.rules');
1703     assert.optionalObject(opts.vms, 'opts.vms');
1704     assert.optionalObject(opts.save, 'opts.save');
1705 
1706     pipeline({
1707     funcs: [
1708         // Determine which platform-specific features are available
1709         function loadFeatures(res, cb) {
1710             features.load({log: log}, cb);
1711         },
1712 
1713         // Generate the ipf files for each VM
1714         function reloadPlan(res, cb) {
1715             prepareIPFdata({
1716                 allVMs: opts.allVMs,
1717                 remoteVMs: opts.allRemoteVMs,
1718                 rules: opts.rules,
1719                 vms: opts.vms
1720             }, log, cb);
1721         },
1722 
1723         // Save the remote VMs
1724         function saveVMs(res, cb) {
1725             if (opts.dryrun || !opts.save || !opts.save.remoteVMs
1726                 || objEmpty(opts.save.remoteVMs)) {
1727                 return cb(null);
1728             }
1729             mod_rvm.save(opts.save.remoteVMs, log, cb);
1730         },
1731 
1732         // Save rule files (if specified)
1733         function save(res, cb) {
1734             if (opts.dryrun || !opts.save || !opts.save.rules
1735                 || opts.save.rules.length === 0) {
1736                 return cb(null);
1737             }
1738             saveRules(opts.save.rules, log, cb);
1739         },
1740 
1741         // Delete rule files (if specified)
1742         function delRules(res, cb) {
1743             if (opts.dryrun || !opts.del || !opts.del.rules
1744                 || opts.del.rules.length === 0) {
1745                 return cb(null);
1746             }
1747             deleteRules(opts.del.rules, log, cb);
1748         },
1749 
1750         // Delete remote VMs (if specified)
1751         function delRVMs(res, cb) {
1752             if (opts.dryrun || !opts.del || !opts.del.rvms
1753                 || opts.del.rvms.length === 0) {
1754                 return cb(null);
1755             }
1756             mod_rvm.del(opts.del.rvms, log, cb);
1757         },
1758 
1759         // Write the new ipf files to disk and restart affected VMs
1760         function ipfData(res, cb) {
1761             saveConfsAndReload(opts, res.reloadPlan, log, cb);
1762         }
1763     ] }, function (err, res) {
1764         if (err) {
1765             callback(err);
1766             return;
1767         }
1768 
1769         var toReturn = {
1770             vms: res.state.ipfData.vms
1771         };
1772 
1773         if (opts.save) {
1774             if (opts.save.rules) {
1775                 toReturn.rules = opts.save.rules.map(function (r) {
1776                     return r.serialize();
1777                 });
1778             }
1779 
1780             if (opts.save.remoteVMs) {
1781                 toReturn.remoteVMs = Object.keys(opts.save.remoteVMs).sort();
1782             }
1783         }
1784 
1785         if (opts.del) {
1786             if (opts.del.rules) {
1787                 toReturn.rules = opts.del.rules.map(function (r) {
1788                     return r.serialize();
1789                 });
1790             }
1791 
1792             if (opts.del.rvms && opts.del.rvms.length !== 0) {
1793                 toReturn.remoteVMs = opts.del.rvms.sort();
1794             }
1795         }
1796 
1797         if (opts.filecontents) {
1798             toReturn.files = res.state.ipfData.files;
1799         }
1800 
1801         callback(null, toReturn);
1802     });
1803 }
1804 
1805 
1806 /**
1807  * Examine the stderr from an ipf command and return true if the zone
1808  * wasn't running at the time
1809  */
1810 function zoneNotRunning(res) {
1811     return res && res.stderr && res.stderr.indexOf(NOT_RUNNING_MSG) !== -1;
1812 }
1813 
1814 
1815 
1816 // --- Exported functions
1817 
1818 
1819 /**
1820  * Functions that touch anything in the following directories:
1821  *
1822  *   - /var/fw (such as /var/fw/rules and /var/fw/vms)
1823  *   - /zones/<uuid>/config (e.g. ipf.conf and ipf6.conf)
1824  *
1825  * Should acquire the appropriate type of lock so that they read and write
1826  * a consistent view of the local firewall rules, and so that parallel
1827  * executions don't run into each other while operating. (See FWAPI-240.)
1828  */
1829 
1830 
1831 /**
1832  * Add rules, local VMs or remote VMs
1833  *
1834  * @param {Object} opts : options
1835  *   - localVMs {Array} : list of local VMs to update
1836  *   - remoteVMs {Array} : list of remote VMs to add
1837  *   - rules {Array} : list of rules
1838  *   - vms {Array} : list of VMs from vmadm
1839  * @param {Function} callback : `f(err, res)`
1840  */
1841 function add(opts, callback) {
1842     try {
1843         validateOpts(opts);
1844         assert.optionalArrayOfObject(opts.rules, 'opts.rules');
1845         assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
1846         assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
1847         assert.optionalString(opts.createdBy, 'opts.createdBy');
1848 
1849         var optRules = opts.rules || [];
1850         var optLocalVMs = opts.localVMs || [];
1851         var optRemoteVMs = opts.remoteVMs || [];
1852         if (optRules.length === 0 && optLocalVMs.length === 0
1853             && optRemoteVMs.length === 0) {
1854             throw new Error(
1855                 'Payload must contain one of: rules, localVMs, remoteVMs');
1856         }
1857     } catch (err) {
1858         return callback(err);
1859     }
1860     var log = util_log.entry(opts, 'add');
1861 
1862     pipeline({
1863     funcs: [
1864         function lock(_, cb) {
1865             mod_lock.acquireExclusiveLock(cb);
1866         },
1867 
1868         function originalRules(_, cb) {
1869             createRules(opts.rules, opts.createdBy, cb);
1870         },
1871 
1872         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
1873 
1874         function disk(_, cb) { loadDataFromDisk(log, cb); },
1875 
1876         // If we're trying to add a rule that already exists and looks
1877         // the same, drop it.
1878         function rules(res, cb) {
1879             getChangingRules(res.originalRules, res.disk.rulesByUUID, cb);
1880         },
1881 
1882         function newRemoteVMs(res, cb) {
1883             mod_rvm.create({ allVMs: res.vms, requireIPs: true, log: log },
1884                 opts.remoteVMs, cb);
1885         },
1886 
1887         // Create remote VMs (if any) from payload
1888         function remoteVMs(res, cb) {
1889             createRemoteVMlookup(res.newRemoteVMs, log, cb);
1890         },
1891 
1892         // Create a combined remote VM lookup of remote VMs on disk plus
1893         // new remote VMs in the payload
1894         function allRemoteVMs(res, cb) {
1895             createRemoteVMlookup([res.disk.remoteVMs, res.newRemoteVMs],
1896                 log, cb);
1897         },
1898 
1899         function localVMs(res, cb) {
1900             lookupVMs(res.vms, opts.localVMs, log, cb);
1901         },
1902 
1903         // Build a table for information about newly added local/remote VMs
1904         function newVMs(res, cb) {
1905             var nvms = clone(res.remoteVMs);
1906             mod_obj.values(res.localVMs).map(mergeIntoLookup.bind(null, nvms));
1907             cb(null, nvms);
1908         },
1909 
1910         function allRules(res, cb) {
1911             return cb(null, dedupRules(res.rules, res.disk.rules));
1912         },
1913 
1914         // Get VMs the added rules affect
1915         function matchingVMs(res, cb) {
1916             filter.vmsByRules({
1917                 log: log,
1918                 rules: res.rules,
1919                 vms: res.vms
1920             }, cb);
1921         },
1922 
1923         // Get rules the added remote VMs affect
1924         function remoteVMrules(res, cb) {
1925             filter.rulesByRVMs(res.remoteVMs, res.allRules, log, cb);
1926         },
1927 
1928         // Get any rules that the added local VMs target
1929         function localVMrules(res, cb) {
1930             filter.rulesByVMs(res.vms, res.localVMs, res.allRules, log, cb);
1931         },
1932 
1933         // Merge the local and remote VM rules, and use that list to find
1934         // the VMs affected.
1935         function localAndRemoteVMsAffected(res, cb) {
1936             var affectedRules = dedupRules(res.localVMrules, res.remoteVMrules)
1937                 .filter(getAffectedRules(res.newVMs, log));
1938             filter.vmsByRules({
1939                 log: log,
1940                 rules: affectedRules,
1941                 vms: res.vms
1942             }, cb);
1943         },
1944 
1945         function mergedVMs(res, cb) {
1946             var ruleVMs = mergeObjects(res.localVMs, res.matchingVMs);
1947             return cb(null, mergeObjects(ruleVMs,
1948                 res.localAndRemoteVMsAffected));
1949         },
1950 
1951         // Get the rules that need to be written out for all VMs, before and
1952         // after the update
1953         function vmRules(res, cb) {
1954             filter.rulesByVMs(res.vms, res.mergedVMs, res.allRules, log, cb);
1955         },
1956 
1957         function apply(res, cb) {
1958             applyChanges({
1959                 allVMs: res.vms,
1960                 dryrun: opts.dryrun,
1961                 filecontents: opts.filecontents,
1962                 allRemoteVMs: res.allRemoteVMs,
1963                 rules: res.vmRules,
1964                 save: {
1965                     rules: res.rules,
1966                     remoteVMs: res.newRemoteVMs
1967                 },
1968                 vms: res.mergedVMs
1969             }, log, cb);
1970         }
1971     ]}, function (err, res) {
1972         mod_lock.releaseLock(res.state.lock);
1973 
1974         if (err) {
1975             util_log.finishErr(log, err, 'add: finish');
1976             return callback(err);
1977         }
1978 
1979         log.debug(res.state.apply, 'add: finish');
1980         return callback(err, res.state.apply);
1981     });
1982 }
1983 
1984 
1985 /**
1986  * Delete rules
1987  *
1988  * @param {Object} opts : options
1989  *   - uuids {Array} : list of rules
1990  *   - vms {Array} : list of VMs from vmadm
1991  * @param {Function} callback : `f(err, res)`
1992  */
1993 function del(opts, callback) {
1994     try {
1995         assert.object(opts, 'opts');
1996         assert.optionalArrayOfString(opts.rvmUUIDs, 'opts.rvmUUIDs');
1997         assert.optionalArrayOfString(opts.uuids, 'opts.uuids');
1998         assert.arrayOfObject(opts.vms, 'vms');
1999 
2000         var rvmUUIDs = opts.rvmUUIDs || [];
2001         var uuids = opts.uuids || [];
2002         if (rvmUUIDs.length === 0 && uuids.length === 0) {
2003             throw new Error(
2004                 'Payload must contain one of: rvmUUIDs, uuids');
2005         }
2006 
2007     } catch (err) {
2008         return callback(err);
2009     }
2010     var log = util_log.entry(opts, 'del');
2011 
2012     pipeline({
2013     funcs: [
2014         function lock(_, cb) {
2015             mod_lock.acquireExclusiveLock(cb);
2016         },
2017         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2018 
2019         function disk(_, cb) { loadDataFromDisk(log, cb); },
2020 
2021         function allRemoteVMs(state, cb) {
2022             createRemoteVMlookup(state.disk.remoteVMs, log, cb);
2023         },
2024 
2025         // Get matching remote VMs
2026         function remoteVMs(state, cb) {
2027             filter.rvmsByUUIDs(state.allRemoteVMs, opts.rvmUUIDs, log, cb);
2028         },
2029 
2030         // Get rules the delted remote VMs affect
2031         function remoteVMrules(res, cb) {
2032             filter.rulesByRVMs(res.remoteVMs.matching, res.disk.rules,
2033                 log, cb);
2034         },
2035 
2036         // Get VMs that are affected by the remote VM rules
2037         function rvmVMs(res, cb) {
2038             filter.vmsByRules({
2039                 log: log,
2040                 rules: res.remoteVMrules,
2041                 vms: res.vms
2042             }, cb);
2043         },
2044 
2045         // Get the deleted rules
2046         function rules(res, cb) {
2047             filter.rulesByUUIDs(res.disk.rules, opts.uuids, log, cb);
2048         },
2049 
2050         // Get VMs the deleted rules affect
2051         function ruleVMs(res, cb) {
2052             filter.vmsByRules({
2053                 log: log,
2054                 rules: res.rules.matching,
2055                 vms: res.vms
2056             }, cb);
2057         },
2058 
2059         // Now find all rules that apply to those VMs, omitting the
2060         // rules that are deleted
2061         function vmRules(res, cb) {
2062             filter.rulesByVMs(res.vms,
2063                 mergeObjects(res.ruleVMs, res.rvmVMs),
2064                 res.rules.notMatching, log, cb);
2065         },
2066 
2067         function apply(res, cb) {
2068             applyChanges({
2069                 allVMs: res.vms,
2070                 dryrun: opts.dryrun,
2071                 filecontents: opts.filecontents,
2072                 allRemoteVMs: res.remoteVMs.notMatching,
2073                 rules: res.vmRules,
2074                 del: {
2075                     rules: res.rules.matching,
2076                     rvms: objEmpty(res.remoteVMs.matching.all) ?
2077                         null : Object.keys(res.remoteVMs.matching.all)
2078                 },
2079                 vms: mergeObjects(res.ruleVMs, res.rvmVMs)
2080             }, log, cb);
2081         }
2082     ]}, function (err, res) {
2083         mod_lock.releaseLock(res.state.lock);
2084 
2085         if (err) {
2086             util_log.finishErr(log, err, 'del: finish');
2087             return callback(err);
2088         }
2089 
2090         log.debug(res.state.apply, 'del: finish');
2091         return callback(err, res.state.apply);
2092     });
2093 }
2094 
2095 
2096 /**
2097  * Returns a remote VM
2098  *
2099  * @param opts {Object} : options:
2100  * - remoteVM {String} : UUID of remote VM to get
2101  * @param callback {Function} : `function (err, rvm)`
2102  */
2103 function getRemoteVM(opts, callback) {
2104     try {
2105         assert.object(opts, 'opts');
2106         assert.string(opts.remoteVM, 'opts.remoteVM');
2107     } catch (err) {
2108         return callback(err);
2109     }
2110     var log = util_log.entry(opts, 'getRemoteVM', true);
2111 
2112     mod_lock.acquireSharedLock(function (lErr, fd) {
2113         if (lErr) {
2114             callback(lErr);
2115             return;
2116         }
2117 
2118         mod_rvm.load(opts.remoteVM, log, function (err, rvm) {
2119             mod_lock.releaseLock(fd);
2120 
2121             if (err) {
2122                 if (err.code == 'ENOENT') {
2123                     // Don't write a log file for "not found"
2124                     log.info(err, 'getRemoteVM: finish');
2125                 } else {
2126                     util_log.finishErr(log, err, 'getRemoteVM: finish');
2127                 }
2128                 return callback(err);
2129             }
2130 
2131             log.debug(rvm, 'getRemoteVM: finish');
2132             return callback(null, rvm);
2133         });
2134     });
2135 }
2136 
2137 
2138 /**
2139  * Returns a rule
2140  *
2141  * @param opts {Object} : options:
2142  * - uuid {String} : UUID of rule to get
2143  * @param callback {Function} : `function (err, rule)`
2144  */
2145 function getRule(opts, callback) {
2146     try {
2147         assert.object(opts, 'opts');
2148         assert.string(opts.uuid, 'opts.uuid');
2149     } catch (err) {
2150         return callback(err);
2151     }
2152     var log = util_log.entry(opts, 'get', true);
2153 
2154     mod_lock.acquireSharedLock(function (lErr, fd) {
2155         if (lErr) {
2156             callback(lErr);
2157             return;
2158         }
2159 
2160         loadRule(opts.uuid, log, function (err, rule) {
2161             mod_lock.releaseLock(fd);
2162 
2163             if (err) {
2164                 if (err.code == 'ENOENT') {
2165                     // Don't write a log file for "not found"
2166                     log.info(err, 'get: finish');
2167                 } else {
2168                     util_log.finishErr(log, err, 'get: finish');
2169                 }
2170                 return callback(err);
2171             }
2172 
2173             var ser = rule.serialize();
2174             log.debug(ser, 'get: finish');
2175             return callback(null, ser);
2176         });
2177     });
2178 }
2179 
2180 
2181 /**
2182  * List remote VMs
2183  */
2184 function listRemoteVMs(opts, callback) {
2185     try {
2186         assert.object(opts, 'opts');
2187     } catch (err) {
2188         return callback(err);
2189     }
2190     var log = util_log.entry(opts, 'listRemoteVMs', true);
2191 
2192     mod_lock.acquireSharedLock(function (lErr, fd) {
2193         if (lErr) {
2194             callback(lErr);
2195             return;
2196         }
2197 
2198         mod_rvm.loadAll(log, function (err, res) {
2199             mod_lock.releaseLock(fd);
2200 
2201             if (err) {
2202                 util_log.finishErr(log, err, 'listRemoteVMs: finish');
2203                 return callback(err);
2204             }
2205 
2206             // XXX: support sorting by other fields, filtering
2207             var sortFn = function _sort(a, b) {
2208                 return (a.uuid > b.uuid) ? 1: -1;
2209             };
2210 
2211             log.debug('listRemoteVMs: finish');
2212             return callback(null, Object.keys(res).map(function (r) {
2213                 return res[r];
2214             }).sort(sortFn));
2215         });
2216     });
2217 }
2218 
2219 
2220 /**
2221  * List rules
2222  */
2223 function listRules(opts, callback) {
2224     try {
2225         assert.object(opts, 'opts');
2226         assert.optionalArrayOfString(opts.fields, 'opts.fields');
2227         if (opts.fields) {
2228             var invalid = [];
2229             opts.fields.forEach(function (f) {
2230                 if (mod_rule.FIELDS.indexOf(f) === -1) {
2231                     invalid.push(f);
2232                 }
2233             });
2234 
2235             if (invalid.length > 0) {
2236                 throw new verror.VError('Invalid display field%s: %s',
2237                     invalid.length == 1 ? '' : 's',
2238                     invalid.sort().join(', '));
2239             }
2240         }
2241     } catch (err) {
2242         return callback(err);
2243     }
2244     var log = util_log.entry(opts, 'list', true);
2245 
2246     mod_lock.acquireSharedLock(function (lErr, fd) {
2247         if (lErr) {
2248             callback(lErr);
2249             return;
2250         }
2251 
2252         loadAllRules(log, function (err, res) {
2253             mod_lock.releaseLock(fd);
2254 
2255             if (err) {
2256                 util_log.finishErr(log, err, 'list: finish');
2257                 return callback(err);
2258             }
2259 
2260             // XXX: support sorting by other fields, filtering
2261             // (eg: enabled=true vm=<uuid>)
2262             var sortFn = function _defaultSort(a, b) {
2263                 return (a.uuid > b.uuid) ? 1: -1;
2264             };
2265             var mapFn = function _defaultMap(r) {
2266                 return r.serialize();
2267             };
2268 
2269             if (opts.fields) {
2270                 var filterFields = opts.fields;
2271                 // If we didn't include uuid in the fields to list, include
2272                 // it here so that we can sort by it - we'll remove it after
2273                 if (opts.fields.indexOf('uuid') === -1) {
2274                     filterFields = opts.fields.concat(['uuid']);
2275                 }
2276 
2277                 mapFn = function _fieldMap(r) {
2278                     return r.serialize(filterFields);
2279                 };
2280             }
2281 
2282             var rules = res.map(mapFn).sort(sortFn);
2283             if (opts.fields && opts.fields.indexOf('uuid') === -1) {
2284                 rules = rules.map(function (r) {
2285                     delete r.uuid;
2286                     return r;
2287                 });
2288             }
2289 
2290             log.debug('list: finish');
2291             return callback(null, rules);
2292         });
2293     });
2294 }
2295 
2296 
2297 /**
2298  * Enable the firewall for a VM. If the VM is running, start ipf for that VM.
2299  *
2300  * @param opts {Object} : options:
2301  * - vms {Array} : array of VM objects (as per VM.js)
2302  * - vm {Object} : VM object for the VM to enable
2303  * - dryrun {Boolean} : don't write any files to disk (Optional)
2304  * - filecontents {Boolean} : return contents of files written to
2305  *   disk (Optional)
2306  * @param callback {Function} `function (err, res)`
2307  * - Where res is an object, optionall containing a files subhash
2308  *   if opts.filecontents is set
2309  */
2310 function enableVM(opts, callback) {
2311     try {
2312         assert.object(opts, 'opts');
2313         assert.object(opts.vm, 'opts.vm');
2314         assert.arrayOfObject(opts.vms, 'opts.vms');
2315     } catch (err) {
2316         return callback(err);
2317     }
2318     var log = util_log.entry(opts, 'enable');
2319 
2320     var vmFilter = {};
2321 
2322     pipeline({
2323     funcs: [
2324         function lock(_, cb) {
2325             mod_lock.acquireExclusiveLock(cb);
2326         },
2327         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2328 
2329         function disk(_, cb) { loadDataFromDisk(log, cb); },
2330 
2331         function getVM(res, cb) {
2332             var vm = res.vms.all[opts.vm.uuid];
2333             if (!vm) {
2334                 return cb(new verror.VError('VM "%s" not found', opts.vm.uuid));
2335             }
2336 
2337             vmFilter[opts.vm.uuid] = vm;
2338             return cb();
2339         },
2340 
2341         // Find all rules that apply to the VM
2342         function vmRules(res, cb) {
2343             filter.rulesByVMs(res.vms, vmFilter, res.disk.rules, log, cb);
2344         },
2345 
2346         function allRemoteVMs(res, cb) {
2347             createRemoteVMlookup(res.disk.remoteVMs, log, cb);
2348         },
2349 
2350         function apply(res, cb) {
2351             applyChanges({
2352                 allVMs: res.vms,
2353                 dryrun: opts.dryrun,
2354                 filecontents: opts.filecontents,
2355                 allRemoteVMs: res.allRemoteVMs,
2356                 rules: res.vmRules,
2357                 vms: vmFilter
2358             }, log, cb);
2359         }
2360     ]}, function _afterEnable(err, res) {
2361         mod_lock.releaseLock(res.state.lock);
2362 
2363         if (err) {
2364             util_log.finishErr(log, err, 'enable: finish');
2365             return callback(err);
2366         }
2367 
2368         var toReturn = res.state.apply;
2369         log.debug(toReturn, 'enable: finish');
2370         return callback(null, toReturn);
2371     });
2372 }
2373 
2374 
2375 /**
2376  * Disable the firewall for a VM. If the VM is running, stop ipf for that VM.
2377  *
2378  * @param opts {Object} : options:
2379  * - vm {Object} : VM object for the VM to disable
2380  * @param callback {Function} `function (err)`
2381  */
2382 function disableVM(opts, callback) {
2383     try {
2384         assert.object(opts, 'opts');
2385         assert.object(opts.vm, 'opts.vm');
2386     } catch (err) {
2387         return callback(err);
2388     }
2389     var log = util_log.entry(opts, 'disable');
2390 
2391     function moveConf(new_fmt, old_fmt, _, cb) {
2392         // Move config out of the way - on zone boot, the firewall
2393         // will start again if it's present
2394         var new_cfg = util.format(new_fmt, opts.vm.zonepath);
2395         var old_cfg = util.format(old_fmt, opts.vm.zonepath);
2396         return fs.rename(new_cfg, old_cfg, function (err) {
2397             // If the file's already gone, that's OK
2398             if (err && err.code !== 'ENOENT') {
2399                 return cb(err);
2400             }
2401 
2402             return cb(null);
2403         });
2404     }
2405 
2406     pipeline({
2407     funcs: [
2408         function lock(_, cb) {
2409             mod_lock.acquireExclusiveLock(cb);
2410         },
2411         moveConf.bind(null, IPF_CONF, IPF_CONF_OLD),
2412         moveConf.bind(null, IPF6_CONF, IPF6_CONF_OLD),
2413         function stop(_, cb) {
2414             if (opts.vm.state !== 'running') {
2415                 log.debug('disableVM: VM "%s": not stopping ipf (state=%s)',
2416                     opts.vm.uuid, opts.vm.state);
2417                 return cb(null);
2418             }
2419 
2420             log.debug('disableVM: stopping ipf for VM "%s"', opts.vm.uuid);
2421             return mod_ipf.stop(opts.vm.uuid, log, cb);
2422         }
2423     ]}, function _afterDisable(err, res) {
2424         mod_lock.releaseLock(res.state.lock);
2425 
2426         if (err) {
2427             util_log.finishErr(log, err, 'disable: finish');
2428             return callback(err);
2429         }
2430 
2431         log.debug('disable: finish');
2432         return callback();
2433     });
2434 }
2435 
2436 
2437 /**
2438  * Gets the firewall status for a VM
2439  *
2440  * @param opts {Object} : options:
2441  * - uuid {String} : VM UUID
2442  * @param callback {Function} `function (err, res)`
2443  */
2444 function vmStatus(opts, callback) {
2445     try {
2446         assert.object(opts, 'opts');
2447         assert.string(opts.uuid, 'opts.uuid');
2448     } catch (err) {
2449         return callback(err);
2450     }
2451     var log = util_log.entry(opts, 'status', true);
2452 
2453     return mod_ipf.status(opts.uuid, log, function (err, res) {
2454         if (err) {
2455             // 'No such device' is returned when the zone is down
2456             if (zoneNotRunning(res)) {
2457                 log.debug({ running: false }, 'status: finish');
2458                 return callback(null, { running: false });
2459             }
2460 
2461             util_log.finishErr(log, err, 'status: finish');
2462             return callback(err);
2463         }
2464 
2465         log.debug(res, 'status: finish');
2466         return callback(null, res);
2467     });
2468 }
2469 
2470 
2471 /**
2472  * Gets the firewall statistics for a VM
2473  *
2474  * @param opts {Object} : options:
2475  * - uuid {String} : VM UUID
2476  * @param callback {Function} `function (err, res)`
2477  */
2478 function vmStats(opts, callback) {
2479     try {
2480         assert.object(opts, 'opts');
2481         assert.string(opts.uuid, 'opts.uuid');
2482     } catch (err) {
2483         return callback(err);
2484     }
2485     var log = util_log.entry(opts, 'stats', true);
2486 
2487     return mod_ipf.ruleStats(opts.uuid, log, function (err, res) {
2488         if (err) {
2489             if (res && res.stderr) {
2490                 // Zone is down
2491                 if (zoneNotRunning(res)) {
2492                     log.debug('stats: finish: zone not running');
2493                     return callback(new verror.VError(
2494                         'Firewall is not running for VM "%s"', opts.uuid));
2495                 }
2496 
2497                 // No rules loaded: return an error if the firewall
2498                 // isn't running
2499                 if (res.stderr.indexOf('empty list') !== -1) {
2500                     return vmStatus(opts, function (err2, res2) {
2501                         if (err2) {
2502                             util_log.finishErr(log, err2, 'stats: finish');
2503                             return callback(err2);
2504                         }
2505 
2506                         if (res2.running) {
2507                             log.debug({ rules: [] }, 'stats: finish');
2508                             return callback(null, { rules: [] });
2509                         } else {
2510                             log.debug('stats: finish: firewall not running');
2511                             return callback(new verror.VError(
2512                                 'Firewall is not running for VM "%s"',
2513                                 opts.uuid));
2514                         }
2515                     });
2516                 }
2517             }
2518 
2519             return callback(err);
2520         }
2521 
2522         log.debug({ rules: res }, 'stats: finish');
2523         return callback(null, { rules: res });
2524     });
2525 }
2526 
2527 
2528 /**
2529  * Update rules, local VMs or remote VMs
2530  *
2531  * @param {Object} opts : options
2532  *   - localVMs {Array} : list of local VMs to update
2533  *   - remoteVMs {Array} : list of remote VMs to update
2534  *   - rules {Array} : list of rules
2535  *   - vms {Array} : list of VMs from vmadm
2536  * @param {Function} callback : `f(err, res)`
2537  */
2538 function update(opts, callback) {
2539     try {
2540         validateOpts(opts);
2541         assert.optionalArrayOfObject(opts.rules, 'opts.rules');
2542         assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
2543         assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
2544         assert.optionalString(opts.createdBy, 'opts.createdBy');
2545 
2546         var optRules = opts.rules || [];
2547         var optLocalVMs = opts.localVMs || [];
2548         var optRemoteVMs = opts.remoteVMs || [];
2549         if (optRules.length === 0 && optLocalVMs.length === 0
2550             && optRemoteVMs.length === 0) {
2551             throw new Error(
2552                 'Payload must contain one of: rules, localVMs, remoteVMs');
2553         }
2554     } catch (err) {
2555         return callback(err);
2556     }
2557     var log = util_log.entry(opts, 'update');
2558 
2559     pipeline({
2560     funcs: [
2561         function lock(_, cb) {
2562             mod_lock.acquireExclusiveLock(cb);
2563         },
2564         function disk(_, cb) { loadDataFromDisk(log, cb); },
2565 
2566         // Make sure the rules exist
2567         function originalRules(res, cb) {
2568             findRules({
2569                 allRules: res.disk.rulesByUUID,
2570                 allowAdds: opts.allowAdds,
2571                 rules: opts.rules
2572             }, log, cb);
2573         },
2574 
2575         // Apply updates to the found rules
2576         function rules(res, cb) {
2577             createUpdatedRules({
2578                 createdBy: opts.createdBy,
2579                 originalRules: res.originalRules,
2580                 updatedRules: opts.rules
2581             }, log, cb);
2582         },
2583 
2584         // Create list of rules that are being replaced
2585         function replacedRules(res, cb) {
2586             cb(null, mod_obj.values(res.originalRules));
2587         },
2588 
2589         // Create the VM lookup
2590         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2591 
2592         // Create remote VMs (if any) from payload
2593         function newRemoteVMs(res, cb) {
2594             mod_rvm.create({ allVMs: res.vms, requireIPs: true, log: log },
2595                 opts.remoteVMs, cb);
2596         },
2597 
2598         // Create a lookup for the new remote VMs
2599         function newRemoteVMsLookup(res, cb) {
2600             createRemoteVMlookup(res.newRemoteVMs, log, cb);
2601         },
2602 
2603         function allRemoteVMs(res, cb) {
2604             createRemoteVMlookup([res.disk.remoteVMs, res.newRemoteVMs],
2605                 log, cb);
2606         },
2607 
2608         // Lookup any local VMs in the payload
2609         function localVMs(res, cb) {
2610             lookupVMs(res.vms, opts.localVMs, log, cb);
2611         },
2612 
2613         // Build a table for information about updated local/remote VMs
2614         function newVMs(res, cb) {
2615             var nvms = clone(res.newRemoteVMsLookup);
2616             mod_obj.values(res.localVMs).map(mergeIntoLookup.bind(null, nvms));
2617             cb(null, nvms);
2618         },
2619 
2620         // Get the VMs the rules applied to before the update
2621         function originalVMs(res, cb) {
2622             filter.vmsByRules({
2623                 log: log,
2624                 rules: res.replacedRules,
2625                 vms: res.vms
2626             }, cb);
2627         },
2628 
2629         // Now get the VMs the updated rules apply to
2630         function matchingVMs(res, cb) {
2631             filter.vmsByRules({
2632                 log: log,
2633                 rules: res.rules,
2634                 vms: res.vms
2635             }, cb);
2636         },
2637 
2638         // Replace the rules with their updated versions
2639         function updatedRules(res, cb) {
2640             return cb(null, dedupRules(res.rules, res.disk.rules));
2641         },
2642 
2643         // Get any rules that the added remote VMs target
2644         function remoteVMrules(res, cb) {
2645             filter.rulesByRVMs(res.newRemoteVMsLookup,
2646                 res.updatedRules, log, cb);
2647         },
2648 
2649         // Get any rules that the added local VMs target
2650         function localVMrules(res, cb) {
2651             filter.rulesByVMs(res.vms, res.localVMs, res.updatedRules, log, cb);
2652         },
2653 
2654         // Merge the local and remote VM rules, and use that list to find
2655         // the VMs affected.
2656         function localAndRemoteVMsAffected(res, cb) {
2657             var affectedRules = dedupRules(res.localVMrules, res.remoteVMrules)
2658                 .filter(getAffectedRules(res.newVMs, log));
2659             filter.vmsByRules({
2660                 log: log,
2661                 rules: affectedRules,
2662                 vms: res.vms
2663             }, cb);
2664         },
2665 
2666         function mergedVMs(res, cb) {
2667             // These are VMs affected by changing rules:
2668             var ruleVMs = mergeObjects(res.originalVMs, res.matchingVMs);
2669             // These are VMs affected by changing VM information:
2670             var updatedVMs = mergeObjects(res.localVMs,
2671                 res.localAndRemoteVMsAffected);
2672             return cb(null, mergeObjects(ruleVMs, updatedVMs));
2673         },
2674 
2675         // Get the rules that need to be written out for all VMs, before and
2676         // after the update
2677         function vmRules(res, cb) {
2678             filter.rulesByVMs(res.vms, res.mergedVMs, res.updatedRules, log,
2679                 cb);
2680         },
2681 
2682         function apply(res, cb) {
2683             applyChanges({
2684                 allVMs: res.vms,
2685                 dryrun: opts.dryrun,
2686                 filecontents: opts.filecontents,
2687                 allRemoteVMs: res.allRemoteVMs,
2688                 rules: res.vmRules,
2689                 save: {
2690                     rules: res.rules,
2691                     remoteVMs: res.newRemoteVMs
2692                 },
2693                 vms: res.mergedVMs
2694             }, log, cb);
2695         }
2696     ]}, function (err, res) {
2697         mod_lock.releaseLock(res.state.lock);
2698 
2699         if (err) {
2700             util_log.finishErr(log, err, 'update: finish');
2701             return callback(err);
2702         }
2703 
2704         log.debug(res.state.apply, 'update: finish');
2705         return callback(err, res.state.apply);
2706     });
2707 }
2708 
2709 
2710 /**
2711  * Given the list of local VMs and a list of rules, return an object with
2712  * the non-local targets on the other side of the rules.
2713  *
2714  * @param opts {Object} : options:
2715  * - vms {Array} : array of VM objects (as per VM.js)
2716  * - rules {Array of Objects} : firewall rules
2717  * @param callback {Function} `function (err, targets)`
2718  * - Where targets is an object like:
2719  *   {
2720  *     tags: { some: ['one', 'two'], other: true },
2721  *     vms: [ '<UUID>' ],
2722  *     allVMs: true
2723  *   }
2724  */
2725 function getRemoteTargets(opts, callback) {
2726     try {
2727         assert.object(opts, 'opts');
2728         assert.arrayOfObject(opts.vms, 'opts.vms');
2729         assert.arrayOfObject(opts.rules, 'opts.rules');
2730 
2731         if (opts.rules.length === 0) {
2732             throw new Error('Must specify rules');
2733         }
2734 
2735     } catch (err) {
2736         return callback(err);
2737     }
2738     var log = util_log.entry(opts, 'remoteTargets', true);
2739 
2740     pipeline({
2741     funcs: [
2742         function lock(_, cb) {
2743             mod_lock.acquireSharedLock(cb);
2744         },
2745         function rules(_, cb) {
2746             createRules(opts.rules, cb);
2747         },
2748         function vms(_, cb) {
2749             createVMlookup(opts.vms, log, cb);
2750         }
2751     ] }, function (err, res) {
2752         mod_lock.releaseLock(res.state.lock);
2753 
2754         if (err) {
2755             util_log.finishErr(log, err, 'remoteTargets: createRules: finish');
2756             return callback(err);
2757         }
2758 
2759         var targets = {};
2760 
2761         for (var r in res.state.rules) {
2762             var rule = res.state.rules[r];
2763 
2764             for (var d in DIRECTIONS) {
2765                 var dir = DIRECTIONS[d];
2766                 addOtherSideRemoteTargets(
2767                     res.state.vms, rule, targets, dir, log);
2768             }
2769         }
2770 
2771         if (hasKey(targets, 'vms')) {
2772             targets.vms = Object.keys(targets.vms);
2773             if (targets.vms.length === 0) {
2774                 delete targets.vms;
2775             }
2776         }
2777 
2778         log.debug(targets, 'remoteTargets: finish');
2779         return callback(null, targets);
2780     });
2781 }
2782 
2783 
2784 /**
2785  * Gets VMs that are affected by a rule
2786  *
2787  * @param opts {Object} : options:
2788  * - vms {Array} : array of VM objects (as per VM.js)
2789  * - rule {UUID or Object} : UUID of pre-existing rule, or a rule object
2790  * - includeDisabled {Boolean, optional} : if set, include VMs that have
2791  *   their firewalls disabled in the search
2792  * @param callback {Function} `function (err, vms)`
2793  * - Where vms is an array of VMs that are affected by that rule
2794  */
2795 function getRuleVMs(opts, callback) {
2796     try {
2797         assert.object(opts, 'opts');
2798         assert.arrayOfObject(opts.vms, 'opts.vms');
2799         assertStringOrObject(opts.rule, 'opts.rule');
2800         assert.optionalBool(opts.includeDisabled, 'opts.includeDisabled');
2801     } catch (err) {
2802         return callback(err);
2803     }
2804     var log = util_log.entry(opts, 'vms', true);
2805 
2806     pipeline({
2807     funcs: [
2808         function lock(_, cb) {
2809             mod_lock.acquireSharedLock(cb);
2810         },
2811         function rules(_, cb) {
2812             if (typeof (opts.rule) === 'string') {
2813                 return loadRule(opts.rule, log, cb);
2814             }
2815 
2816             createRules([ opts.rule ], cb);
2817         },
2818         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2819         function ruleVMs(state, cb) {
2820             if (!Array.isArray(state.rules)) {
2821                 state.rules = [ state.rules ];
2822             }
2823 
2824             filter.vmsByRules({
2825                 includeDisabled: opts.includeDisabled,
2826                 log: log,
2827                 rules: state.rules,
2828                 vms: state.vms
2829             }, cb);
2830         }
2831     ]}, function (err, res) {
2832         mod_lock.releaseLock(res.state.lock);
2833 
2834         if (err) {
2835             util_log.finishErr(log, err, 'vms: finish');
2836             return callback(err);
2837         }
2838 
2839         var matched = Object.keys(res.state.ruleVMs);
2840         log.debug(matched, 'vms: finish');
2841         return callback(null, matched);
2842     });
2843 }
2844 
2845 
2846 /**
2847  * Gets rules that apply to a Remote VM
2848  *
2849  * @param opts {Object} : options:
2850  * - vms {Array} : array of VM objects (as per VM.js)
2851  * - vm {UUID} : UUID of VM to get the rules for
2852  * @param callback {Function} `function (err, rules)`
2853  * - Where rules is an array of rules that apply to the VM
2854  */
2855 function getRemoteVMrules(opts, callback) {
2856     try {
2857         assert.object(opts, 'opts');
2858         assertStringOrObject(opts.remoteVM, 'opts.remoteVM');
2859         assert.arrayOfObject(opts.vms, 'opts.vms');
2860     } catch (err) {
2861         return callback(err);
2862     }
2863     var log = util_log.entry(opts, 'rvmRules', true);
2864 
2865     pipeline({
2866     funcs: [
2867         function lock(_, cb) {
2868             mod_lock.acquireSharedLock(cb);
2869         },
2870         function allRules(_, cb) { loadAllRules(log, cb); },
2871         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2872         function rvm(_, cb) {
2873             if (typeof (opts.remoteVM) === 'object') {
2874                 return cb(null, opts.remoteVM);
2875             }
2876 
2877             return mod_rvm.load(opts.remoteVM, log, cb);
2878         },
2879         function rvms(state, cb) {
2880             mod_rvm.create({ allVMs: state.vms, requireIPs: false, log: log },
2881                 [ state.rvm ], function (e, rvmList) {
2882                 if (e) {
2883                     return cb(e);
2884                 }
2885 
2886                 createRemoteVMlookup(rvmList, log, cb);
2887             });
2888         },
2889         function rvmRules(state, cb) {
2890             filter.rulesByRVMs(state.rvms, state.allRules, log, cb);
2891         }
2892     ]}, function (err, res) {
2893         mod_lock.releaseLock(res.state.lock);
2894 
2895         if (err) {
2896             util_log.finishErr(log, err, 'rvmRules: finish (vm=%s)',
2897                 opts.remoteVM);
2898             return callback(err);
2899         }
2900 
2901         var toReturn = res.state.rvmRules.map(function (r) {
2902             return r.serialize();
2903         });
2904 
2905         log.debug(toReturn, 'rvmRules: finish (vm=%s)', opts.remoteVM);
2906         return callback(null, toReturn);
2907     });
2908 }
2909 
2910 
2911 /**
2912  * Gets rules that apply to a VM
2913  *
2914  * @param opts {Object} : options:
2915  * - vms {Array} : array of VM objects (as per VM.js)
2916  * - vm {UUID} : UUID of VM to get the rules for
2917  * @param callback {Function} `function (err, rules)`
2918  * - Where rules is an array of rules that apply to the VM
2919  */
2920 function getVMrules(opts, callback) {
2921     try {
2922         assert.object(opts, 'opts');
2923         assert.string(opts.vm, 'opts.vm');
2924         assert.arrayOfObject(opts.vms, 'opts.vms');
2925     } catch (err) {
2926         return callback(err);
2927     }
2928     var log = util_log.entry(opts, 'vmRules', true);
2929 
2930     var toFind = {};
2931     toFind[opts.vm] = opts.vm;
2932 
2933     pipeline({
2934     funcs: [
2935         function lock(_, cb) {
2936             mod_lock.acquireSharedLock(cb);
2937         },
2938         function allRules(_, cb) { loadAllRules(log, cb); },
2939         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2940         function vmRules(state, cb) {
2941             filter.rulesByVMs(state.vms, toFind, state.allRules, log, cb);
2942         }
2943     ]}, function (err, res) {
2944         mod_lock.releaseLock(res.state.lock);
2945 
2946         if (err) {
2947             util_log.finishErr(log, err, 'vmRules: finish (vm=%s)', opts.vm);
2948             return callback(err);
2949         }
2950 
2951         var toReturn = res.state.vmRules.map(function (r) {
2952             return r.serialize();
2953         });
2954 
2955         log.debug(toReturn, 'vmRules: finish (vm=%s)', opts.vm);
2956         return callback(null, toReturn);
2957     });
2958 }
2959 
2960 
2961 /**
2962  * Validates an add / update payload
2963  *
2964  * @param opts {Object} : options:
2965  *   - localVMs {Array} : list of local VMs
2966  *   - remoteVMs {Array} : list of remote VMs
2967  *   - rules {Array} : list of rules
2968  *   - vms {Array} : array of VM objects (as per VM.js)
2969  * @param callback {Function} `function (err, rules)`
2970  * - Where rules is an array of rules that apply to the VM
2971  */
2972 function validatePayload(opts, callback) {
2973     try {
2974         assert.object(opts, 'opts');
2975         assert.arrayOfObject(opts.vms, 'opts.vms');
2976         assert.optionalArrayOfObject(opts.rules, 'opts.rules');
2977         assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
2978         assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
2979 
2980         var optRules = opts.rules || [];
2981         var optLocalVMs = opts.localVMs || [];
2982         var optRemoteVMs = opts.remoteVMs || [];
2983         if (optRules.length === 0 && optLocalVMs.length === 0
2984             && optRemoteVMs.length === 0) {
2985             throw new Error(
2986                 'Payload must contain one of: rules, localVMs, remoteVMs');
2987         }
2988     } catch (err) {
2989         return callback(err);
2990     }
2991     var log = util_log.entry(opts, 'validatePayload');
2992 
2993     pipeline({
2994     funcs: [
2995         function lock(_, cb) {
2996             mod_lock.acquireSharedLock(cb);
2997         },
2998         function rules(_, cb) {
2999             createRules(opts.rules, cb);
3000         },
3001         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
3002         function remoteVMs(_, cb) { mod_rvm.loadAll(log, cb); },
3003         function newRemoteVMs(state, cb) {
3004             mod_rvm.create({ allVMs: state.vms, requireIPs: true, log: log },
3005                 opts.remoteVMs, cb);
3006         },
3007         // Create a combined remote VM lookup of remote VMs on disk plus
3008         // new remote VMs in the payload
3009         function allRemoteVMs(state, cb) {
3010             createRemoteVMlookup([state.remoteVMs, state.newRemoteVMs],
3011                 log, cb);
3012         },
3013 
3014         function validate(state, cb) {
3015             validateRules(state.vms, state.allRemoteVMs, state.rules, log, cb);
3016         }
3017     ]}, function (err, res) {
3018         mod_lock.releaseLock(res.state.lock);
3019 
3020         if (err) {
3021             util_log.finishErr(log, err, 'validatePayload: finish');
3022             return callback(err);
3023         }
3024 
3025         log.debug(opts.payload, 'validatePayload: finish');
3026         return callback();
3027     });
3028 }
3029 
3030 
3031 
3032 module.exports = {
3033     _setOldIPF: mod_ipf._setOld,
3034     add: add,
3035     del: del,
3036     disable: disableVM,
3037     enable: enableVM,
3038     get: getRule,
3039     getRVM: getRemoteVM,
3040     list: listRules,
3041     listRVMs: listRemoteVMs,
3042     remoteTargets: getRemoteTargets,
3043     rvmRules: getRemoteVMrules,
3044     setBunyan: util_log.setBunyan,
3045     stats: vmStats,
3046     status: vmStatus,
3047     update: update,
3048     validatePayload: validatePayload,
3049     VM_FIELDS: VM_FIELDS,
3050     vmRules: getVMrules,
3051     vms: getRuleVMs
3052 };