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 sortObj = {
1090         action: rule.action,
1091         direction: dir,
1092         priority: rule.priority,
1093         protocol: rule.protocol,
1094         header: util.format('\n# rule=%s, version=%s, %s=%s',
1095             rule.uuid, rule.version, opts.type, opts.value),
1096         v4text: [],
1097         v6text: [],
1098         targets: targets,
1099         protoTargets: rule.protoTargets,
1100         type: opts.type,
1101         uuid: rule.uuid,
1102         value: opts.value,
1103         version: rule.version,
1104         uuidTag: (features.feature[FEATURE_INOUT_UUID] && rule.uuid) ?
1105             sprintf(' set-tag(uuid=%s)', rule.uuid) : ''
1106     };
1107 
1108     if (opts.type === 'wildcard' && opts.value === 'any') {
1109         rule.protoTargets.forEach(function (t) {
1110             var wild = util.format('%s %s quick proto %s from any to any %s',
1111                 rule.action === 'allow' ? 'pass' : 'block',
1112                 dir === 'from' ? 'out' : 'in',
1113                 ipfProto,
1114                 protoTarget(rule, t));
1115             if (rule.protocol !== 'icmp6')
1116                 sortObj.v4text.push(wild);
1117             if (rule.protocol !== 'icmp')
1118                 sortObj.v6text.push(wild);
1119         });
1120 
1121         return sortObj;
1122     }
1123 
1124     targets.forEach(function (target) {
1125         var isv6 = target.indexOf(':') !== -1;
1126 
1127         // Don't generate rules for ICMPv4/IPv6 or ICMPv6/IPv4
1128         if ((isv6 && rule.protocol === 'icmp')
1129             || (!isv6 && rule.protocol === 'icmp6')) {
1130             return;
1131         }
1132 
1133         var text = isv6 ? sortObj.v6text : sortObj.v4text;
1134 
1135         rule.protoTargets.forEach(function (t) {
1136             text.push(
1137                 util.format('%s %s quick proto %s from %s to %s %s',
1138                     rule.action === 'allow' ? 'pass' : 'block',
1139                     dir === 'from' ? 'out' : 'in',
1140                     ipfProto,
1141                     dir === 'to' ? target : 'any',
1142                     dir === 'to' ? 'any' : target,
1143                     protoTarget(rule, t)))
1144         });
1145     });
1146 
1147     return sortObj;
1148 }
1149 
1150 
1151 /**
1152  * Returns an object containing all ipf files to be written to disk, based
1153  * on the given rules
1154  *
1155  * @param opts {Object} :
1156  * - @param allVMs {Object} : VM lookup table, as returned by createVMlookup()
1157  * - @param remoteVMs {Array} : array of remote VM objects (optional)
1158  * - @param rules {Array} : array of rule objects
1159  * - @param vms {Array} : object mapping VM UUIDs to VM objects. All VMs in
1160  *   this object will have conf files written. This covers the case where
1161  *   a rule used to target a VM, but no longer does, so we want to write the
1162  *   config minus the rule that no longer applies.
1163  * @param callback {Function} `function (err)`
1164  */
1165 function prepareIPFdata(opts, log, callback) {
1166     var allVMs = opts.allVMs;
1167     var date = new Date();
1168     var rules = opts.rules;
1169     var vms = opts.vms;
1170     var remoteVMs = opts.remoteVMs || { ips: {}, vms: {}, tags: {} };
1171 
1172     log.debug({ vms: vms, rules: rules }, 'prepareIPFdata: entry');
1173 
1174     var conf = {};
1175     if (vms) {
1176         conf = Object.keys(vms).reduce(function (acc, v) {
1177             // If the VM's firewall is disabled, we don't need to write out
1178             // rules for it
1179             if (allVMs.all[v].enabled) {
1180                 acc[v] = [];
1181             }
1182             return acc;
1183         }, {});
1184     }
1185 
1186     /* Gather the VMs targeted on each side of every enabled rule. */
1187     var targetVMs = rules.map(function (rule) {
1188         if (!rule.enabled) {
1189             return null;
1190         }
1191 
1192         return {
1193             from: vmsOnSide(allVMs, rule, 'from', log),
1194             to: vmsOnSide(allVMs, rule, 'to', log)
1195         };
1196     });
1197 
1198     /*
1199      * If we block outbound traffic for a protocol, make sure to also track
1200      * inbound state for anything allowed, so that we'll allow response
1201      * packets.
1202      *
1203      * We could just always enable state tracking for all of our inbound
1204      * allow rules, but state tracking can get pretty expensive. There's no
1205      * need to penalize all firewall users.
1206      */
1207     var keepInboundState = { };
1208     rules.forEach(function (rule, i) {
1209         if (!rule.enabled || rule.action === 'allow') {
1210             return;
1211         }
1212 
1213         targetVMs[i].from.forEach(function (uuid) {
1214             if (!hasKey(keepInboundState, uuid)) {
1215                 keepInboundState[uuid] = {};
1216             }
1217             keepInboundState[uuid][rule.protocol] = true;
1218         });
1219     });
1220 
1221     rules.forEach(function (rule, i) {
1222         if (!rule.enabled) {
1223             return;
1224         }
1225 
1226         DIRECTIONS.forEach(function (dir) {
1227             // XXX: add to errors here if missing
1228 
1229             // Default outgoing policy is 'allow' and default incoming policy
1230             // is 'block', so these are effectively no-ops:
1231             if (noRulesNeeded(dir, rule)) {
1232                 return;
1233             }
1234 
1235             var otherSideRules =
1236                 rulesFromOtherSide(rule, dir, allVMs, remoteVMs);
1237 
1238             targetVMs[i][dir].forEach(function (uuid) {
1239                 /*
1240                  * If the VM's firewall is disabled, we don't need to write out
1241                  * rules for it.
1242                  */
1243                 if (!allVMs.all[uuid].enabled || !hasKey(conf, uuid)) {
1244                     return;
1245                 }
1246 
1247                 conf[uuid] = conf[uuid].concat(otherSideRules);
1248             });
1249         });
1250     });
1251 
1252     var toReturn = [];
1253     for (var vm in conf) {
1254         var rulesIncluded = {};
1255         var ipf4Conf = [
1256             '# DO NOT EDIT THIS FILE. THIS FILE IS AUTO-GENERATED BY fwadm(1M)',
1257             '# AND MAY BE OVERWRITTEN AT ANY TIME.',
1258             '#',
1259             '# File generated at ' + date.toString(),
1260             '#',
1261             ''];
1262         var ipf6Conf = ipf4Conf.slice();
1263         var iks = hasKey(keepInboundState, vm) ? keepInboundState[vm] : {};
1264 
1265         conf[vm].sort(compareRules).forEach(function (sortObj) {
1266             assert.string(sortObj.uuidTag, 'sortObj.uuidTag');
1267             var ktxt = KEEP_FRAGS;
1268             if (sortObj.uuidTag !== ''
1269                 || (sortObj.direction === 'from' && sortObj.action === 'allow')
1270                 || (sortObj.direction === 'to' && iks[sortObj.protocol])) {
1271                 ktxt += KEEP_STATE + sortObj.uuidTag;
1272             }
1273 
1274             if (!hasKey(rulesIncluded, sortObj.uuid)) {
1275                 rulesIncluded[sortObj.uuid] = [];
1276             }
1277             rulesIncluded[sortObj.uuid].push(sortObj.direction);
1278 
1279             ipf4Conf.push(sortObj.header);
1280             ipf6Conf.push(sortObj.header);
1281 
1282             sortObj.v4text.forEach(function (line) {
1283                 ipf4Conf.push(line + ktxt);
1284             });
1285             sortObj.v6text.forEach(function (line) {
1286                 ipf6Conf.push(line + ktxt);
1287             });
1288         });
1289 
1290         log.debug(rulesIncluded, 'VM %s: generated ipf(6).conf', vm);
1291 
1292         var v4rules = ipf4Conf.concat(v4fallbacks);
1293         var v6rules = ipf6Conf.concat(v6fallbacks);
1294 
1295         toReturn.push({
1296             uuid: vm,
1297             zonepath: allVMs.all[vm].zonepath,
1298             v4text: v4rules.join('\n') + '\n',
1299             v6text: v6rules.join('\n') + '\n'
1300         });
1301     }
1302 
1303     return callback(null, toReturn);
1304 }
1305 
1306 
1307 /**
1308  * Returns an array of the UUIDs of VMs on the given side of a rule
1309  */
1310 function vmsOnSide(allVMs, rule, dir, log) {
1311     var matching = [];
1312 
1313     ['vms', 'tags', 'wildcards'].forEach(function (walkType) {
1314         rule[dir][walkType].forEach(function (t) {
1315             var type = walkType;
1316             var value;
1317             if (typeof (t) !== 'string') {
1318                 value = t[1];
1319                 t = t[0];
1320                 type = 'tagValues';
1321             }
1322 
1323             if (type === 'wildcards' && t === 'any') {
1324                 return;
1325             }
1326 
1327             if (!allVMs[type] || !hasKey(allVMs[type], t)) {
1328                 log.debug('No matching VMs found in lookup for %s=%s', type, t);
1329                 return;
1330             }
1331 
1332             var vmList = allVMs[type][t];
1333             if (value !== undefined) {
1334                 if (!hasKey(vmList, value)) {
1335                     return;
1336                 }
1337                 vmList = vmList[value];
1338             }
1339 
1340             Object.keys(vmList).forEach(function (uuid) {
1341                 if (hasKey(rule, 'owner_uuid')
1342                     && (rule.owner_uuid != vmList[uuid].owner_uuid)) {
1343                     return;
1344                 }
1345 
1346                 matching.push(uuid);
1347             });
1348         });
1349     });
1350 
1351     return matching;
1352 }
1353 
1354 
1355 /**
1356  * Returns the ipf rules for the opposite side of a rule
1357  */
1358 function rulesFromOtherSide(rule, dir, localVMs, remoteVMs) {
1359     var otherSide = dir === 'from' ? 'to' : 'from';
1360     var ipfRules = [];
1361 
1362     if (rule[otherSide].wildcards.indexOf('any') !== -1) {
1363         ipfRules.push(ipfRuleObj({
1364             rule: rule,
1365             direction: dir,
1366             targets: [ '0.0.0.0' ],
1367             type: 'wildcard',
1368             value: 'any'
1369         }));
1370 
1371         return ipfRules;
1372     }
1373 
1374     // IPs and subnets don't need looking up in the local or remote VM
1375     // lookup objects, so just them as-is
1376     ['ip', 'subnet'].forEach(function (type) {
1377         rule[otherSide][type + 's'].forEach(function (value) {
1378             ipfRules.push(ipfRuleObj({
1379                 rule: rule,
1380                 direction: dir,
1381                 targets: value,
1382                 type: type,
1383                 value: value
1384             }));
1385         });
1386     });
1387 
1388     // Lookup the VMs in the local and remove VM lookups, and add their IPs
1389     // accordingly
1390     ['tag', 'vm', 'wildcard'].forEach(function (type) {
1391         var typePlural = type + 's';
1392         rule[otherSide][typePlural].forEach(function (value) {
1393             var lookupType = type;
1394             var lookupTypePlural = typePlural;
1395             var t;
1396 
1397             if (typeof (value) !== 'string') {
1398                 t = value[1];
1399                 value = value[0];
1400                 lookupType = 'tagValue';
1401                 lookupTypePlural = 'tagValues';
1402             }
1403 
1404             if (lookupTypePlural === 'wildcards' && value === 'any') {
1405                 return;
1406             }
1407 
1408             [localVMs, remoteVMs].forEach(function (lookup) {
1409                 if (!hasKey(lookup, lookupTypePlural)
1410                     || !hasKey(lookup[lookupTypePlural], value)) {
1411                     return;
1412                 }
1413 
1414                 var vmList = lookup[lookupTypePlural][value];
1415                 if (t !== undefined) {
1416                     if (!hasKey(vmList, t)) {
1417                         return;
1418                     }
1419                     vmList = vmList[t];
1420                 }
1421 
1422                 forEachKey(vmList, function (uuid, vm) {
1423                     if (rule.owner_uuid && vm.owner_uuid
1424                         && vm.owner_uuid != rule.owner_uuid) {
1425                         return;
1426                     }
1427 
1428                     if (vm.ips.length === 0) {
1429                         return;
1430                     }
1431 
1432                     ipfRules.push(ipfRuleObj({
1433                         rule: rule,
1434                         direction: dir,
1435                         targets: vm.ips,
1436                         type: lookupType,
1437                         value: value
1438                     }));
1439                 });
1440             });
1441         });
1442     });
1443 
1444     return ipfRules;
1445 }
1446 
1447 
1448 /**
1449  * Gets remote targets from the other side of the rule and adds them to
1450  * the targets object
1451  */
1452 function addOtherSideRemoteTargets(vms, rule, targets, dir, log) {
1453     var matching = vmsOnSide(vms, rule, dir, log);
1454     if (matching.length === 0) {
1455         return;
1456     }
1457 
1458     var otherSide = dir === 'from' ? 'to' : 'from';
1459     if (rule[otherSide].tags.length !== 0) {
1460         if (!hasKey(targets, 'tags')) {
1461             targets.tags = {};
1462         }
1463 
1464         // All tags (no value) wins out over tags with
1465         // a value. If multiple values for the same tag
1466         // are present, return them as an array
1467         rule[otherSide].tags.forEach(function (tag) {
1468             var key = tag;
1469             var val = true;
1470             if (typeof (tag) !== 'string') {
1471                 key = tag[0];
1472                 val = tag[1];
1473             }
1474 
1475             if (!hasKey(targets.tags, key)) {
1476                 targets.tags[key] = val;
1477             } else {
1478                 if (targets.tags[key] !== true) {
1479                     if (val === true) {
1480                         targets.tags[key] = val;
1481                     } else {
1482                         if (!Array.isArray(targets.tags[key])) {
1483                             targets.tags[key] = [ targets.tags[key] ];
1484                         }
1485 
1486                         if (targets.tags[key].indexOf(val) === -1) {
1487                             targets.tags[key].push(val);
1488                         }
1489                     }
1490                 }
1491             }
1492         });
1493     }
1494 
1495     if (rule[otherSide].vms.length !== 0) {
1496         if (!hasKey(targets, 'vms')) {
1497             targets.vms = {};
1498         }
1499 
1500         rule[otherSide].vms.forEach(function (vm) {
1501             // Don't add if it's a local VM
1502             if (!hasKey(vms.all, vm)) {
1503                 targets.vms[vm] = true;
1504             }
1505         });
1506     }
1507 
1508     if (rule[otherSide].wildcards.indexOf('vmall') !== -1) {
1509         targets.allVMs = true;
1510     }
1511 }
1512 
1513 
1514 /**
1515  * Carefully move the new ipfilter configuration file into place. It's
1516  * important to make sure that if we fail or crash at any point that we
1517  * leave a file in place, since its presence is what determines whether
1518  * to enable the firewall at zone boot.
1519  */
1520 function replaceIPFconf(file, data, ver, callback) {
1521     var tempFile = util.format('%s.%s', file, ver);
1522     var oldFile = util.format('%s.old', file);
1523 
1524     vasync.pipeline({
1525     funcs: [
1526         function _write(_, cb) {
1527             fs.writeFile(tempFile, data, cb);
1528         },
1529         function _unlinkOld(_, cb) {
1530             fs.unlink(oldFile, function (err) {
1531                 if (err && err.code === 'ENOENT') {
1532                     cb(null);
1533                     return;
1534                 }
1535 
1536                 cb(err);
1537             });
1538         },
1539         function _linkOld(_, cb) {
1540             fs.link(file, oldFile, function (err) {
1541                 if (err && err.code === 'ENOENT') {
1542                     cb(null);
1543                     return;
1544                 }
1545 
1546                 cb(err);
1547             });
1548         },
1549         function _renameTemp(_, cb) {
1550             fs.rename(tempFile, file, cb);
1551         }
1552     ]}, callback);
1553 }
1554 
1555 
1556 /**
1557  * Saves all of the generated ipfilter rules in ipfData to disk. We handle
1558  * each VM separately in parallel, so that failures for one don't impact
1559  * reloading others. For example, a VM may have filled up its disk, and we
1560  * now can't write out its configuration, or a VM may have stopped on us
1561  * before we had a chance to run ipf(1M) on it.
1562  */
1563 function saveConfsAndReload(opts, ipfData, log, callback) {
1564     var ver = Date.now(0) + '.' + sprintf('%06d', process.pid);
1565     var files = {};
1566     var uuids = [];
1567 
1568     vasync.forEachParallel({
1569         inputs: ipfData,
1570         func: function (vm, cb) {
1571             uuids.push(vm.uuid);
1572 
1573             vasync.pipeline({
1574             funcs: [
1575                 // Write the new ipf.conf for IPv4 rules:
1576                 function writeV4(_, cb2) {
1577                     var filename = util.format(IPF_CONF, vm.zonepath);
1578                     files[filename] = vm.v4text;
1579 
1580                     if (opts.dryrun) {
1581                         cb2(null);
1582                         return;
1583                     }
1584 
1585                     replaceIPFconf(filename, vm.v4text, ver, cb2);
1586                 },
1587 
1588                 // Write the new ipf6.conf for IPv6 rules:
1589                 function writeV6(_, cb2) {
1590                     var filename = util.format(IPF6_CONF, vm.zonepath);
1591                     files[filename] = vm.v6text;
1592 
1593                     if (opts.dryrun) {
1594                         cb2(null);
1595                         return;
1596                     }
1597 
1598                     replaceIPFconf(filename, vm.v6text, ver, cb2);
1599                 },
1600 
1601                 // Restart the VM's firewall:
1602                 function reload(_, cb2) {
1603                     if (opts.dryrun) {
1604                         cb2(null);
1605                         return;
1606                     }
1607 
1608                     restartFirewall(opts.allVMs, vm.uuid, log, cb2);
1609                 }
1610             ]}, cb);
1611         }
1612     }, function (err) {
1613         if (err) {
1614             callback(err);
1615             return;
1616         }
1617 
1618         callback(null, {
1619             files: files,
1620             vms: uuids
1621         });
1622     });
1623 }
1624 
1625 
1626 /**
1627  * Restart the given VMs firewall.
1628  *
1629  * @param vms {Object}: VM lookup table, as returned by createVMlookup()
1630  * @param uuid {UUID}: The UUID of the target VM
1631  * @param callback {Function} `function (err)`
1632  */
1633 function restartFirewall(vms, uuid, log, cb) {
1634     if (!vms.all[uuid].enabled || vms.all[uuid].state !== 'running') {
1635         log.debug('restartFirewalls: VM "%s": not restarting '
1636             + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
1637             vms.all[uuid].state);
1638         cb(null);
1639         return;
1640     }
1641 
1642     log.debug('restartFirewalls: reloading firewall for VM "%s" '
1643         + '(enabled=%s, state=%s)', uuid, vms.all[uuid].enabled,
1644         vms.all[uuid].state);
1645 
1646     // Reload the firewall, and start it if necessary.
1647     reloadIPF({ vm: uuid, zonepath: vms.all[uuid].zonepath }, log,
1648         function (err, res) {
1649         if (err && zoneNotRunning(res)) {
1650             /*
1651              * An error starting the firewall due to the zone not
1652              * running isn't really an error.
1653              */
1654             cb();
1655             return;
1656         }
1657 
1658         cb(err);
1659     });
1660 }
1661 
1662 
1663 /**
1664  * Applies firewall changes:
1665  * - saves / deletes rule files as needed
1666  * - writes out ipf conf files
1667  * - starts or restarts ipf in VMs
1668  *
1669  * @param {Object} opts :
1670  *   - allRemoteVMs {Object} : VM lookup object of all remote VMs
1671  *   - allVMs {Object} : VM lookup object of all local VMs
1672  *   - del {Object} : Objects to delete from disk:
1673  *     - rules {Array of Objects} : rules objects to delete
1674  *     - rvms {Array of Objects} : remote VM UUIDs to delete
1675  *   - dryRun {Bool} : if true, no files will be written or firewalls reloaded
1676  *   - rules {Array of Objects} : rules to write out
1677  *   - save {Object} : Objects to save to disk:
1678  *     - rules {Array of Objects} : rule objects to save
1679  *     - remoteVMs {Array of Objects} : remote VM objects to save
1680  *   - vms {Object} : Mapping of UUID to VM object - VMs to write out
1681  *     firewalls for, regardless of whether or not rules affect them
1682  *     (necessary for catching the case where a VM used to have rules that
1683  *     applied to it but no longer does)
1684  */
1685 function applyChanges(opts, log, callback) {
1686     log.trace(opts, 'applyChanges: entry');
1687 
1688     assert.object(opts, 'opts');
1689     assert.optionalObject(opts.allRemoteVMs, 'opts.allRemoteVMs');
1690     assert.optionalObject(opts.allVMs, 'opts.allVMs');
1691     assert.optionalObject(opts.del, 'opts.del');
1692     assert.optionalArrayOfObject(opts.rules, 'opts.rules');
1693     assert.optionalObject(opts.vms, 'opts.vms');
1694     assert.optionalObject(opts.save, 'opts.save');
1695 
1696     pipeline({
1697     funcs: [
1698         // Determine which platform-specific features are available
1699         function loadFeatures(res, cb) {
1700             features.load({log: log}, cb);
1701         },
1702 
1703         // Generate the ipf files for each VM
1704         function reloadPlan(res, cb) {
1705             prepareIPFdata({
1706                 allVMs: opts.allVMs,
1707                 remoteVMs: opts.allRemoteVMs,
1708                 rules: opts.rules,
1709                 vms: opts.vms
1710             }, log, cb);
1711         },
1712 
1713         // Save the remote VMs
1714         function saveVMs(res, cb) {
1715             if (opts.dryrun || !opts.save || !opts.save.remoteVMs
1716                 || objEmpty(opts.save.remoteVMs)) {
1717                 return cb(null);
1718             }
1719             mod_rvm.save(opts.save.remoteVMs, log, cb);
1720         },
1721 
1722         // Save rule files (if specified)
1723         function save(res, cb) {
1724             if (opts.dryrun || !opts.save || !opts.save.rules
1725                 || opts.save.rules.length === 0) {
1726                 return cb(null);
1727             }
1728             saveRules(opts.save.rules, log, cb);
1729         },
1730 
1731         // Delete rule files (if specified)
1732         function delRules(res, cb) {
1733             if (opts.dryrun || !opts.del || !opts.del.rules
1734                 || opts.del.rules.length === 0) {
1735                 return cb(null);
1736             }
1737             deleteRules(opts.del.rules, log, cb);
1738         },
1739 
1740         // Delete remote VMs (if specified)
1741         function delRVMs(res, cb) {
1742             if (opts.dryrun || !opts.del || !opts.del.rvms
1743                 || opts.del.rvms.length === 0) {
1744                 return cb(null);
1745             }
1746             mod_rvm.del(opts.del.rvms, log, cb);
1747         },
1748 
1749         // Write the new ipf files to disk and restart affected VMs
1750         function ipfData(res, cb) {
1751             saveConfsAndReload(opts, res.reloadPlan, log, cb);
1752         }
1753     ] }, function (err, res) {
1754         if (err) {
1755             callback(err);
1756             return;
1757         }
1758 
1759         var toReturn = {
1760             vms: res.state.ipfData.vms
1761         };
1762 
1763         if (opts.save) {
1764             if (opts.save.rules) {
1765                 toReturn.rules = opts.save.rules.map(function (r) {
1766                     return r.serialize();
1767                 });
1768             }
1769 
1770             if (opts.save.remoteVMs) {
1771                 toReturn.remoteVMs = Object.keys(opts.save.remoteVMs).sort();
1772             }
1773         }
1774 
1775         if (opts.del) {
1776             if (opts.del.rules) {
1777                 toReturn.rules = opts.del.rules.map(function (r) {
1778                     return r.serialize();
1779                 });
1780             }
1781 
1782             if (opts.del.rvms && opts.del.rvms.length !== 0) {
1783                 toReturn.remoteVMs = opts.del.rvms.sort();
1784             }
1785         }
1786 
1787         if (opts.filecontents) {
1788             toReturn.files = res.state.ipfData.files;
1789         }
1790 
1791         callback(null, toReturn);
1792     });
1793 }
1794 
1795 
1796 /**
1797  * Examine the stderr from an ipf command and return true if the zone
1798  * wasn't running at the time
1799  */
1800 function zoneNotRunning(res) {
1801     return res && res.stderr && res.stderr.indexOf(NOT_RUNNING_MSG) !== -1;
1802 }
1803 
1804 
1805 
1806 // --- Exported functions
1807 
1808 
1809 /**
1810  * Functions that touch anything in the following directories:
1811  *
1812  *   - /var/fw (such as /var/fw/rules and /var/fw/vms)
1813  *   - /zones/<uuid>/config (e.g. ipf.conf and ipf6.conf)
1814  *
1815  * Should acquire the appropriate type of lock so that they read and write
1816  * a consistent view of the local firewall rules, and so that parallel
1817  * executions don't run into each other while operating. (See FWAPI-240.)
1818  */
1819 
1820 
1821 /**
1822  * Add rules, local VMs or remote VMs
1823  *
1824  * @param {Object} opts : options
1825  *   - localVMs {Array} : list of local VMs to update
1826  *   - remoteVMs {Array} : list of remote VMs to add
1827  *   - rules {Array} : list of rules
1828  *   - vms {Array} : list of VMs from vmadm
1829  * @param {Function} callback : `f(err, res)`
1830  */
1831 function add(opts, callback) {
1832     try {
1833         validateOpts(opts);
1834         assert.optionalArrayOfObject(opts.rules, 'opts.rules');
1835         assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
1836         assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
1837         assert.optionalString(opts.createdBy, 'opts.createdBy');
1838 
1839         var optRules = opts.rules || [];
1840         var optLocalVMs = opts.localVMs || [];
1841         var optRemoteVMs = opts.remoteVMs || [];
1842         if (optRules.length === 0 && optLocalVMs.length === 0
1843             && optRemoteVMs.length === 0) {
1844             throw new Error(
1845                 'Payload must contain one of: rules, localVMs, remoteVMs');
1846         }
1847     } catch (err) {
1848         return callback(err);
1849     }
1850     var log = util_log.entry(opts, 'add');
1851 
1852     pipeline({
1853     funcs: [
1854         function lock(_, cb) {
1855             mod_lock.acquireExclusiveLock(cb);
1856         },
1857 
1858         function originalRules(_, cb) {
1859             createRules(opts.rules, opts.createdBy, cb);
1860         },
1861 
1862         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
1863 
1864         function disk(_, cb) { loadDataFromDisk(log, cb); },
1865 
1866         // If we're trying to add a rule that already exists and looks
1867         // the same, drop it.
1868         function rules(res, cb) {
1869             getChangingRules(res.originalRules, res.disk.rulesByUUID, cb);
1870         },
1871 
1872         function newRemoteVMs(res, cb) {
1873             mod_rvm.create({ allVMs: res.vms, requireIPs: true, log: log },
1874                 opts.remoteVMs, cb);
1875         },
1876 
1877         // Create remote VMs (if any) from payload
1878         function remoteVMs(res, cb) {
1879             createRemoteVMlookup(res.newRemoteVMs, log, cb);
1880         },
1881 
1882         // Create a combined remote VM lookup of remote VMs on disk plus
1883         // new remote VMs in the payload
1884         function allRemoteVMs(res, cb) {
1885             createRemoteVMlookup([res.disk.remoteVMs, res.newRemoteVMs],
1886                 log, cb);
1887         },
1888 
1889         function localVMs(res, cb) {
1890             lookupVMs(res.vms, opts.localVMs, log, cb);
1891         },
1892 
1893         // Build a table for information about newly added local/remote VMs
1894         function newVMs(res, cb) {
1895             var nvms = clone(res.remoteVMs);
1896             mod_obj.values(res.localVMs).map(mergeIntoLookup.bind(null, nvms));
1897             cb(null, nvms);
1898         },
1899 
1900         function allRules(res, cb) {
1901             return cb(null, dedupRules(res.rules, res.disk.rules));
1902         },
1903 
1904         // Get VMs the added rules affect
1905         function matchingVMs(res, cb) {
1906             filter.vmsByRules({
1907                 log: log,
1908                 rules: res.rules,
1909                 vms: res.vms
1910             }, cb);
1911         },
1912 
1913         // Get rules the added remote VMs affect
1914         function remoteVMrules(res, cb) {
1915             filter.rulesByRVMs(res.remoteVMs, res.allRules, log, cb);
1916         },
1917 
1918         // Get any rules that the added local VMs target
1919         function localVMrules(res, cb) {
1920             filter.rulesByVMs(res.vms, res.localVMs, res.allRules, log, cb);
1921         },
1922 
1923         // Merge the local and remote VM rules, and use that list to find
1924         // the VMs affected.
1925         function localAndRemoteVMsAffected(res, cb) {
1926             var affectedRules = dedupRules(res.localVMrules, res.remoteVMrules)
1927                 .filter(getAffectedRules(res.newVMs, log));
1928             filter.vmsByRules({
1929                 log: log,
1930                 rules: affectedRules,
1931                 vms: res.vms
1932             }, cb);
1933         },
1934 
1935         function mergedVMs(res, cb) {
1936             var ruleVMs = mergeObjects(res.localVMs, res.matchingVMs);
1937             return cb(null, mergeObjects(ruleVMs,
1938                 res.localAndRemoteVMsAffected));
1939         },
1940 
1941         // Get the rules that need to be written out for all VMs, before and
1942         // after the update
1943         function vmRules(res, cb) {
1944             filter.rulesByVMs(res.vms, res.mergedVMs, res.allRules, log, cb);
1945         },
1946 
1947         function apply(res, cb) {
1948             applyChanges({
1949                 allVMs: res.vms,
1950                 dryrun: opts.dryrun,
1951                 filecontents: opts.filecontents,
1952                 allRemoteVMs: res.allRemoteVMs,
1953                 rules: res.vmRules,
1954                 save: {
1955                     rules: res.rules,
1956                     remoteVMs: res.newRemoteVMs
1957                 },
1958                 vms: res.mergedVMs
1959             }, log, cb);
1960         }
1961     ]}, function (err, res) {
1962         mod_lock.releaseLock(res.state.lock);
1963 
1964         if (err) {
1965             util_log.finishErr(log, err, 'add: finish');
1966             return callback(err);
1967         }
1968 
1969         log.debug(res.state.apply, 'add: finish');
1970         return callback(err, res.state.apply);
1971     });
1972 }
1973 
1974 
1975 /**
1976  * Delete rules
1977  *
1978  * @param {Object} opts : options
1979  *   - uuids {Array} : list of rules
1980  *   - vms {Array} : list of VMs from vmadm
1981  * @param {Function} callback : `f(err, res)`
1982  */
1983 function del(opts, callback) {
1984     try {
1985         assert.object(opts, 'opts');
1986         assert.optionalArrayOfString(opts.rvmUUIDs, 'opts.rvmUUIDs');
1987         assert.optionalArrayOfString(opts.uuids, 'opts.uuids');
1988         assert.arrayOfObject(opts.vms, 'vms');
1989 
1990         var rvmUUIDs = opts.rvmUUIDs || [];
1991         var uuids = opts.uuids || [];
1992         if (rvmUUIDs.length === 0 && uuids.length === 0) {
1993             throw new Error(
1994                 'Payload must contain one of: rvmUUIDs, uuids');
1995         }
1996 
1997     } catch (err) {
1998         return callback(err);
1999     }
2000     var log = util_log.entry(opts, 'del');
2001 
2002     pipeline({
2003     funcs: [
2004         function lock(_, cb) {
2005             mod_lock.acquireExclusiveLock(cb);
2006         },
2007         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2008 
2009         function disk(_, cb) { loadDataFromDisk(log, cb); },
2010 
2011         function allRemoteVMs(state, cb) {
2012             createRemoteVMlookup(state.disk.remoteVMs, log, cb);
2013         },
2014 
2015         // Get matching remote VMs
2016         function remoteVMs(state, cb) {
2017             filter.rvmsByUUIDs(state.allRemoteVMs, opts.rvmUUIDs, log, cb);
2018         },
2019 
2020         // Get rules the delted remote VMs affect
2021         function remoteVMrules(res, cb) {
2022             filter.rulesByRVMs(res.remoteVMs.matching, res.disk.rules,
2023                 log, cb);
2024         },
2025 
2026         // Get VMs that are affected by the remote VM rules
2027         function rvmVMs(res, cb) {
2028             filter.vmsByRules({
2029                 log: log,
2030                 rules: res.remoteVMrules,
2031                 vms: res.vms
2032             }, cb);
2033         },
2034 
2035         // Get the deleted rules
2036         function rules(res, cb) {
2037             filter.rulesByUUIDs(res.disk.rules, opts.uuids, log, cb);
2038         },
2039 
2040         // Get VMs the deleted rules affect
2041         function ruleVMs(res, cb) {
2042             filter.vmsByRules({
2043                 log: log,
2044                 rules: res.rules.matching,
2045                 vms: res.vms
2046             }, cb);
2047         },
2048 
2049         // Now find all rules that apply to those VMs, omitting the
2050         // rules that are deleted
2051         function vmRules(res, cb) {
2052             filter.rulesByVMs(res.vms,
2053                 mergeObjects(res.ruleVMs, res.rvmVMs),
2054                 res.rules.notMatching, log, cb);
2055         },
2056 
2057         function apply(res, cb) {
2058             applyChanges({
2059                 allVMs: res.vms,
2060                 dryrun: opts.dryrun,
2061                 filecontents: opts.filecontents,
2062                 allRemoteVMs: res.remoteVMs.notMatching,
2063                 rules: res.vmRules,
2064                 del: {
2065                     rules: res.rules.matching,
2066                     rvms: objEmpty(res.remoteVMs.matching.all) ?
2067                         null : Object.keys(res.remoteVMs.matching.all)
2068                 },
2069                 vms: mergeObjects(res.ruleVMs, res.rvmVMs)
2070             }, log, cb);
2071         }
2072     ]}, function (err, res) {
2073         mod_lock.releaseLock(res.state.lock);
2074 
2075         if (err) {
2076             util_log.finishErr(log, err, 'del: finish');
2077             return callback(err);
2078         }
2079 
2080         log.debug(res.state.apply, 'del: finish');
2081         return callback(err, res.state.apply);
2082     });
2083 }
2084 
2085 
2086 /**
2087  * Returns a remote VM
2088  *
2089  * @param opts {Object} : options:
2090  * - remoteVM {String} : UUID of remote VM to get
2091  * @param callback {Function} : `function (err, rvm)`
2092  */
2093 function getRemoteVM(opts, callback) {
2094     try {
2095         assert.object(opts, 'opts');
2096         assert.string(opts.remoteVM, 'opts.remoteVM');
2097     } catch (err) {
2098         return callback(err);
2099     }
2100     var log = util_log.entry(opts, 'getRemoteVM', true);
2101 
2102     mod_lock.acquireSharedLock(function (lErr, fd) {
2103         if (lErr) {
2104             callback(lErr);
2105             return;
2106         }
2107 
2108         mod_rvm.load(opts.remoteVM, log, function (err, rvm) {
2109             mod_lock.releaseLock(fd);
2110 
2111             if (err) {
2112                 if (err.code == 'ENOENT') {
2113                     // Don't write a log file for "not found"
2114                     log.info(err, 'getRemoteVM: finish');
2115                 } else {
2116                     util_log.finishErr(log, err, 'getRemoteVM: finish');
2117                 }
2118                 return callback(err);
2119             }
2120 
2121             log.debug(rvm, 'getRemoteVM: finish');
2122             return callback(null, rvm);
2123         });
2124     });
2125 }
2126 
2127 
2128 /**
2129  * Returns a rule
2130  *
2131  * @param opts {Object} : options:
2132  * - uuid {String} : UUID of rule to get
2133  * @param callback {Function} : `function (err, rule)`
2134  */
2135 function getRule(opts, callback) {
2136     try {
2137         assert.object(opts, 'opts');
2138         assert.string(opts.uuid, 'opts.uuid');
2139     } catch (err) {
2140         return callback(err);
2141     }
2142     var log = util_log.entry(opts, 'get', true);
2143 
2144     mod_lock.acquireSharedLock(function (lErr, fd) {
2145         if (lErr) {
2146             callback(lErr);
2147             return;
2148         }
2149 
2150         loadRule(opts.uuid, log, function (err, rule) {
2151             mod_lock.releaseLock(fd);
2152 
2153             if (err) {
2154                 if (err.code == 'ENOENT') {
2155                     // Don't write a log file for "not found"
2156                     log.info(err, 'get: finish');
2157                 } else {
2158                     util_log.finishErr(log, err, 'get: finish');
2159                 }
2160                 return callback(err);
2161             }
2162 
2163             var ser = rule.serialize();
2164             log.debug(ser, 'get: finish');
2165             return callback(null, ser);
2166         });
2167     });
2168 }
2169 
2170 
2171 /**
2172  * List remote VMs
2173  */
2174 function listRemoteVMs(opts, callback) {
2175     try {
2176         assert.object(opts, 'opts');
2177     } catch (err) {
2178         return callback(err);
2179     }
2180     var log = util_log.entry(opts, 'listRemoteVMs', true);
2181 
2182     mod_lock.acquireSharedLock(function (lErr, fd) {
2183         if (lErr) {
2184             callback(lErr);
2185             return;
2186         }
2187 
2188         mod_rvm.loadAll(log, function (err, res) {
2189             mod_lock.releaseLock(fd);
2190 
2191             if (err) {
2192                 util_log.finishErr(log, err, 'listRemoteVMs: finish');
2193                 return callback(err);
2194             }
2195 
2196             // XXX: support sorting by other fields, filtering
2197             var sortFn = function _sort(a, b) {
2198                 return (a.uuid > b.uuid) ? 1: -1;
2199             };
2200 
2201             log.debug('listRemoteVMs: finish');
2202             return callback(null, Object.keys(res).map(function (r) {
2203                 return res[r];
2204             }).sort(sortFn));
2205         });
2206     });
2207 }
2208 
2209 
2210 /**
2211  * List rules
2212  */
2213 function listRules(opts, callback) {
2214     try {
2215         assert.object(opts, 'opts');
2216         assert.optionalArrayOfString(opts.fields, 'opts.fields');
2217         if (opts.fields) {
2218             var invalid = [];
2219             opts.fields.forEach(function (f) {
2220                 if (mod_rule.FIELDS.indexOf(f) === -1) {
2221                     invalid.push(f);
2222                 }
2223             });
2224 
2225             if (invalid.length > 0) {
2226                 throw new verror.VError('Invalid display field%s: %s',
2227                     invalid.length == 1 ? '' : 's',
2228                     invalid.sort().join(', '));
2229             }
2230         }
2231     } catch (err) {
2232         return callback(err);
2233     }
2234     var log = util_log.entry(opts, 'list', true);
2235 
2236     mod_lock.acquireSharedLock(function (lErr, fd) {
2237         if (lErr) {
2238             callback(lErr);
2239             return;
2240         }
2241 
2242         loadAllRules(log, function (err, res) {
2243             mod_lock.releaseLock(fd);
2244 
2245             if (err) {
2246                 util_log.finishErr(log, err, 'list: finish');
2247                 return callback(err);
2248             }
2249 
2250             // XXX: support sorting by other fields, filtering
2251             // (eg: enabled=true vm=<uuid>)
2252             var sortFn = function _defaultSort(a, b) {
2253                 return (a.uuid > b.uuid) ? 1: -1;
2254             };
2255             var mapFn = function _defaultMap(r) {
2256                 return r.serialize();
2257             };
2258 
2259             if (opts.fields) {
2260                 var filterFields = opts.fields;
2261                 // If we didn't include uuid in the fields to list, include
2262                 // it here so that we can sort by it - we'll remove it after
2263                 if (opts.fields.indexOf('uuid') === -1) {
2264                     filterFields = opts.fields.concat(['uuid']);
2265                 }
2266 
2267                 mapFn = function _fieldMap(r) {
2268                     return r.serialize(filterFields);
2269                 };
2270             }
2271 
2272             var rules = res.map(mapFn).sort(sortFn);
2273             if (opts.fields && opts.fields.indexOf('uuid') === -1) {
2274                 rules = rules.map(function (r) {
2275                     delete r.uuid;
2276                     return r;
2277                 });
2278             }
2279 
2280             log.debug('list: finish');
2281             return callback(null, rules);
2282         });
2283     });
2284 }
2285 
2286 
2287 /**
2288  * Enable the firewall for a VM. If the VM is running, start ipf for that VM.
2289  *
2290  * @param opts {Object} : options:
2291  * - vms {Array} : array of VM objects (as per VM.js)
2292  * - vm {Object} : VM object for the VM to enable
2293  * - dryrun {Boolean} : don't write any files to disk (Optional)
2294  * - filecontents {Boolean} : return contents of files written to
2295  *   disk (Optional)
2296  * @param callback {Function} `function (err, res)`
2297  * - Where res is an object, optionall containing a files subhash
2298  *   if opts.filecontents is set
2299  */
2300 function enableVM(opts, callback) {
2301     try {
2302         assert.object(opts, 'opts');
2303         assert.object(opts.vm, 'opts.vm');
2304         assert.arrayOfObject(opts.vms, 'opts.vms');
2305     } catch (err) {
2306         return callback(err);
2307     }
2308     var log = util_log.entry(opts, 'enable');
2309 
2310     var vmFilter = {};
2311 
2312     pipeline({
2313     funcs: [
2314         function lock(_, cb) {
2315             mod_lock.acquireExclusiveLock(cb);
2316         },
2317         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2318 
2319         function disk(_, cb) { loadDataFromDisk(log, cb); },
2320 
2321         function getVM(res, cb) {
2322             var vm = res.vms.all[opts.vm.uuid];
2323             if (!vm) {
2324                 return cb(new verror.VError('VM "%s" not found', opts.vm.uuid));
2325             }
2326 
2327             vmFilter[opts.vm.uuid] = vm;
2328             return cb();
2329         },
2330 
2331         // Find all rules that apply to the VM
2332         function vmRules(res, cb) {
2333             filter.rulesByVMs(res.vms, vmFilter, res.disk.rules, log, cb);
2334         },
2335 
2336         function allRemoteVMs(res, cb) {
2337             createRemoteVMlookup(res.disk.remoteVMs, log, cb);
2338         },
2339 
2340         function apply(res, cb) {
2341             applyChanges({
2342                 allVMs: res.vms,
2343                 dryrun: opts.dryrun,
2344                 filecontents: opts.filecontents,
2345                 allRemoteVMs: res.allRemoteVMs,
2346                 rules: res.vmRules,
2347                 vms: vmFilter
2348             }, log, cb);
2349         }
2350     ]}, function _afterEnable(err, res) {
2351         mod_lock.releaseLock(res.state.lock);
2352 
2353         if (err) {
2354             util_log.finishErr(log, err, 'enable: finish');
2355             return callback(err);
2356         }
2357 
2358         var toReturn = res.state.apply;
2359         log.debug(toReturn, 'enable: finish');
2360         return callback(null, toReturn);
2361     });
2362 }
2363 
2364 
2365 /**
2366  * Disable the firewall for a VM. If the VM is running, stop ipf for that VM.
2367  *
2368  * @param opts {Object} : options:
2369  * - vm {Object} : VM object for the VM to disable
2370  * @param callback {Function} `function (err)`
2371  */
2372 function disableVM(opts, callback) {
2373     try {
2374         assert.object(opts, 'opts');
2375         assert.object(opts.vm, 'opts.vm');
2376     } catch (err) {
2377         return callback(err);
2378     }
2379     var log = util_log.entry(opts, 'disable');
2380 
2381     function moveConf(new_fmt, old_fmt, _, cb) {
2382         // Move config out of the way - on zone boot, the firewall
2383         // will start again if it's present
2384         var new_cfg = util.format(new_fmt, opts.vm.zonepath);
2385         var old_cfg = util.format(old_fmt, opts.vm.zonepath);
2386         return fs.rename(new_cfg, old_cfg, function (err) {
2387             // If the file's already gone, that's OK
2388             if (err && err.code !== 'ENOENT') {
2389                 return cb(err);
2390             }
2391 
2392             return cb(null);
2393         });
2394     }
2395 
2396     pipeline({
2397     funcs: [
2398         function lock(_, cb) {
2399             mod_lock.acquireExclusiveLock(cb);
2400         },
2401         moveConf.bind(null, IPF_CONF, IPF_CONF_OLD),
2402         moveConf.bind(null, IPF6_CONF, IPF6_CONF_OLD),
2403         function stop(_, cb) {
2404             if (opts.vm.state !== 'running') {
2405                 log.debug('disableVM: VM "%s": not stopping ipf (state=%s)',
2406                     opts.vm.uuid, opts.vm.state);
2407                 return cb(null);
2408             }
2409 
2410             log.debug('disableVM: stopping ipf for VM "%s"', opts.vm.uuid);
2411             return mod_ipf.stop(opts.vm.uuid, log, cb);
2412         }
2413     ]}, function _afterDisable(err, res) {
2414         mod_lock.releaseLock(res.state.lock);
2415 
2416         if (err) {
2417             util_log.finishErr(log, err, 'disable: finish');
2418             return callback(err);
2419         }
2420 
2421         log.debug('disable: finish');
2422         return callback();
2423     });
2424 }
2425 
2426 
2427 /**
2428  * Gets the firewall status for a VM
2429  *
2430  * @param opts {Object} : options:
2431  * - uuid {String} : VM UUID
2432  * @param callback {Function} `function (err, res)`
2433  */
2434 function vmStatus(opts, callback) {
2435     try {
2436         assert.object(opts, 'opts');
2437         assert.string(opts.uuid, 'opts.uuid');
2438     } catch (err) {
2439         return callback(err);
2440     }
2441     var log = util_log.entry(opts, 'status', true);
2442 
2443     return mod_ipf.status(opts.uuid, log, function (err, res) {
2444         if (err) {
2445             // 'No such device' is returned when the zone is down
2446             if (zoneNotRunning(res)) {
2447                 log.debug({ running: false }, 'status: finish');
2448                 return callback(null, { running: false });
2449             }
2450 
2451             util_log.finishErr(log, err, 'status: finish');
2452             return callback(err);
2453         }
2454 
2455         log.debug(res, 'status: finish');
2456         return callback(null, res);
2457     });
2458 }
2459 
2460 
2461 /**
2462  * Gets the firewall statistics for a VM
2463  *
2464  * @param opts {Object} : options:
2465  * - uuid {String} : VM UUID
2466  * @param callback {Function} `function (err, res)`
2467  */
2468 function vmStats(opts, callback) {
2469     try {
2470         assert.object(opts, 'opts');
2471         assert.string(opts.uuid, 'opts.uuid');
2472     } catch (err) {
2473         return callback(err);
2474     }
2475     var log = util_log.entry(opts, 'stats', true);
2476 
2477     return mod_ipf.ruleStats(opts.uuid, log, function (err, res) {
2478         if (err) {
2479             if (res && res.stderr) {
2480                 // Zone is down
2481                 if (zoneNotRunning(res)) {
2482                     log.debug('stats: finish: zone not running');
2483                     return callback(new verror.VError(
2484                         'Firewall is not running for VM "%s"', opts.uuid));
2485                 }
2486 
2487                 // No rules loaded: return an error if the firewall
2488                 // isn't running
2489                 if (res.stderr.indexOf('empty list') !== -1) {
2490                     return vmStatus(opts, function (err2, res2) {
2491                         if (err2) {
2492                             util_log.finishErr(log, err2, 'stats: finish');
2493                             return callback(err2);
2494                         }
2495 
2496                         if (res2.running) {
2497                             log.debug({ rules: [] }, 'stats: finish');
2498                             return callback(null, { rules: [] });
2499                         } else {
2500                             log.debug('stats: finish: firewall not running');
2501                             return callback(new verror.VError(
2502                                 'Firewall is not running for VM "%s"',
2503                                 opts.uuid));
2504                         }
2505                     });
2506                 }
2507             }
2508 
2509             return callback(err);
2510         }
2511 
2512         log.debug({ rules: res }, 'stats: finish');
2513         return callback(null, { rules: res });
2514     });
2515 }
2516 
2517 
2518 /**
2519  * Update rules, local VMs or remote VMs
2520  *
2521  * @param {Object} opts : options
2522  *   - localVMs {Array} : list of local VMs to update
2523  *   - remoteVMs {Array} : list of remote VMs to update
2524  *   - rules {Array} : list of rules
2525  *   - vms {Array} : list of VMs from vmadm
2526  * @param {Function} callback : `f(err, res)`
2527  */
2528 function update(opts, callback) {
2529     try {
2530         validateOpts(opts);
2531         assert.optionalArrayOfObject(opts.rules, 'opts.rules');
2532         assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
2533         assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
2534         assert.optionalString(opts.createdBy, 'opts.createdBy');
2535 
2536         var optRules = opts.rules || [];
2537         var optLocalVMs = opts.localVMs || [];
2538         var optRemoteVMs = opts.remoteVMs || [];
2539         if (optRules.length === 0 && optLocalVMs.length === 0
2540             && optRemoteVMs.length === 0) {
2541             throw new Error(
2542                 'Payload must contain one of: rules, localVMs, remoteVMs');
2543         }
2544     } catch (err) {
2545         return callback(err);
2546     }
2547     var log = util_log.entry(opts, 'update');
2548 
2549     pipeline({
2550     funcs: [
2551         function lock(_, cb) {
2552             mod_lock.acquireExclusiveLock(cb);
2553         },
2554         function disk(_, cb) { loadDataFromDisk(log, cb); },
2555 
2556         // Make sure the rules exist
2557         function originalRules(res, cb) {
2558             findRules({
2559                 allRules: res.disk.rulesByUUID,
2560                 allowAdds: opts.allowAdds,
2561                 rules: opts.rules
2562             }, log, cb);
2563         },
2564 
2565         // Apply updates to the found rules
2566         function rules(res, cb) {
2567             createUpdatedRules({
2568                 createdBy: opts.createdBy,
2569                 originalRules: res.originalRules,
2570                 updatedRules: opts.rules
2571             }, log, cb);
2572         },
2573 
2574         // Create list of rules that are being replaced
2575         function replacedRules(res, cb) {
2576             cb(null, mod_obj.values(res.originalRules));
2577         },
2578 
2579         // Create the VM lookup
2580         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2581 
2582         // Create remote VMs (if any) from payload
2583         function newRemoteVMs(res, cb) {
2584             mod_rvm.create({ allVMs: res.vms, requireIPs: true, log: log },
2585                 opts.remoteVMs, cb);
2586         },
2587 
2588         // Create a lookup for the new remote VMs
2589         function newRemoteVMsLookup(res, cb) {
2590             createRemoteVMlookup(res.newRemoteVMs, log, cb);
2591         },
2592 
2593         function allRemoteVMs(res, cb) {
2594             createRemoteVMlookup([res.disk.remoteVMs, res.newRemoteVMs],
2595                 log, cb);
2596         },
2597 
2598         // Lookup any local VMs in the payload
2599         function localVMs(res, cb) {
2600             lookupVMs(res.vms, opts.localVMs, log, cb);
2601         },
2602 
2603         // Build a table for information about updated local/remote VMs
2604         function newVMs(res, cb) {
2605             var nvms = clone(res.newRemoteVMsLookup);
2606             mod_obj.values(res.localVMs).map(mergeIntoLookup.bind(null, nvms));
2607             cb(null, nvms);
2608         },
2609 
2610         // Get the VMs the rules applied to before the update
2611         function originalVMs(res, cb) {
2612             filter.vmsByRules({
2613                 log: log,
2614                 rules: res.replacedRules,
2615                 vms: res.vms
2616             }, cb);
2617         },
2618 
2619         // Now get the VMs the updated rules apply to
2620         function matchingVMs(res, cb) {
2621             filter.vmsByRules({
2622                 log: log,
2623                 rules: res.rules,
2624                 vms: res.vms
2625             }, cb);
2626         },
2627 
2628         // Replace the rules with their updated versions
2629         function updatedRules(res, cb) {
2630             return cb(null, dedupRules(res.rules, res.disk.rules));
2631         },
2632 
2633         // Get any rules that the added remote VMs target
2634         function remoteVMrules(res, cb) {
2635             filter.rulesByRVMs(res.newRemoteVMsLookup,
2636                 res.updatedRules, log, cb);
2637         },
2638 
2639         // Get any rules that the added local VMs target
2640         function localVMrules(res, cb) {
2641             filter.rulesByVMs(res.vms, res.localVMs, res.updatedRules, log, cb);
2642         },
2643 
2644         // Merge the local and remote VM rules, and use that list to find
2645         // the VMs affected.
2646         function localAndRemoteVMsAffected(res, cb) {
2647             var affectedRules = dedupRules(res.localVMrules, res.remoteVMrules)
2648                 .filter(getAffectedRules(res.newVMs, log));
2649             filter.vmsByRules({
2650                 log: log,
2651                 rules: affectedRules,
2652                 vms: res.vms
2653             }, cb);
2654         },
2655 
2656         function mergedVMs(res, cb) {
2657             // These are VMs affected by changing rules:
2658             var ruleVMs = mergeObjects(res.originalVMs, res.matchingVMs);
2659             // These are VMs affected by changing VM information:
2660             var updatedVMs = mergeObjects(res.localVMs,
2661                 res.localAndRemoteVMsAffected);
2662             return cb(null, mergeObjects(ruleVMs, updatedVMs));
2663         },
2664 
2665         // Get the rules that need to be written out for all VMs, before and
2666         // after the update
2667         function vmRules(res, cb) {
2668             filter.rulesByVMs(res.vms, res.mergedVMs, res.updatedRules, log,
2669                 cb);
2670         },
2671 
2672         function apply(res, cb) {
2673             applyChanges({
2674                 allVMs: res.vms,
2675                 dryrun: opts.dryrun,
2676                 filecontents: opts.filecontents,
2677                 allRemoteVMs: res.allRemoteVMs,
2678                 rules: res.vmRules,
2679                 save: {
2680                     rules: res.rules,
2681                     remoteVMs: res.newRemoteVMs
2682                 },
2683                 vms: res.mergedVMs
2684             }, log, cb);
2685         }
2686     ]}, function (err, res) {
2687         mod_lock.releaseLock(res.state.lock);
2688 
2689         if (err) {
2690             util_log.finishErr(log, err, 'update: finish');
2691             return callback(err);
2692         }
2693 
2694         log.debug(res.state.apply, 'update: finish');
2695         return callback(err, res.state.apply);
2696     });
2697 }
2698 
2699 
2700 /**
2701  * Given the list of local VMs and a list of rules, return an object with
2702  * the non-local targets on the other side of the rules.
2703  *
2704  * @param opts {Object} : options:
2705  * - vms {Array} : array of VM objects (as per VM.js)
2706  * - rules {Array of Objects} : firewall rules
2707  * @param callback {Function} `function (err, targets)`
2708  * - Where targets is an object like:
2709  *   {
2710  *     tags: { some: ['one', 'two'], other: true },
2711  *     vms: [ '<UUID>' ],
2712  *     allVMs: true
2713  *   }
2714  */
2715 function getRemoteTargets(opts, callback) {
2716     try {
2717         assert.object(opts, 'opts');
2718         assert.arrayOfObject(opts.vms, 'opts.vms');
2719         assert.arrayOfObject(opts.rules, 'opts.rules');
2720 
2721         if (opts.rules.length === 0) {
2722             throw new Error('Must specify rules');
2723         }
2724 
2725     } catch (err) {
2726         return callback(err);
2727     }
2728     var log = util_log.entry(opts, 'remoteTargets', true);
2729 
2730     pipeline({
2731     funcs: [
2732         function lock(_, cb) {
2733             mod_lock.acquireSharedLock(cb);
2734         },
2735         function rules(_, cb) {
2736             createRules(opts.rules, cb);
2737         },
2738         function vms(_, cb) {
2739             createVMlookup(opts.vms, log, cb);
2740         }
2741     ] }, function (err, res) {
2742         mod_lock.releaseLock(res.state.lock);
2743 
2744         if (err) {
2745             util_log.finishErr(log, err, 'remoteTargets: createRules: finish');
2746             return callback(err);
2747         }
2748 
2749         var targets = {};
2750 
2751         for (var r in res.state.rules) {
2752             var rule = res.state.rules[r];
2753 
2754             for (var d in DIRECTIONS) {
2755                 var dir = DIRECTIONS[d];
2756                 addOtherSideRemoteTargets(
2757                     res.state.vms, rule, targets, dir, log);
2758             }
2759         }
2760 
2761         if (hasKey(targets, 'vms')) {
2762             targets.vms = Object.keys(targets.vms);
2763             if (targets.vms.length === 0) {
2764                 delete targets.vms;
2765             }
2766         }
2767 
2768         log.debug(targets, 'remoteTargets: finish');
2769         return callback(null, targets);
2770     });
2771 }
2772 
2773 
2774 /**
2775  * Gets VMs that are affected by a rule
2776  *
2777  * @param opts {Object} : options:
2778  * - vms {Array} : array of VM objects (as per VM.js)
2779  * - rule {UUID or Object} : UUID of pre-existing rule, or a rule object
2780  * - includeDisabled {Boolean, optional} : if set, include VMs that have
2781  *   their firewalls disabled in the search
2782  * @param callback {Function} `function (err, vms)`
2783  * - Where vms is an array of VMs that are affected by that rule
2784  */
2785 function getRuleVMs(opts, callback) {
2786     try {
2787         assert.object(opts, 'opts');
2788         assert.arrayOfObject(opts.vms, 'opts.vms');
2789         assertStringOrObject(opts.rule, 'opts.rule');
2790         assert.optionalBool(opts.includeDisabled, 'opts.includeDisabled');
2791     } catch (err) {
2792         return callback(err);
2793     }
2794     var log = util_log.entry(opts, 'vms', true);
2795 
2796     pipeline({
2797     funcs: [
2798         function lock(_, cb) {
2799             mod_lock.acquireSharedLock(cb);
2800         },
2801         function rules(_, cb) {
2802             if (typeof (opts.rule) === 'string') {
2803                 return loadRule(opts.rule, log, cb);
2804             }
2805 
2806             createRules([ opts.rule ], cb);
2807         },
2808         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2809         function ruleVMs(state, cb) {
2810             if (!Array.isArray(state.rules)) {
2811                 state.rules = [ state.rules ];
2812             }
2813 
2814             filter.vmsByRules({
2815                 includeDisabled: opts.includeDisabled,
2816                 log: log,
2817                 rules: state.rules,
2818                 vms: state.vms
2819             }, cb);
2820         }
2821     ]}, function (err, res) {
2822         mod_lock.releaseLock(res.state.lock);
2823 
2824         if (err) {
2825             util_log.finishErr(log, err, 'vms: finish');
2826             return callback(err);
2827         }
2828 
2829         var matched = Object.keys(res.state.ruleVMs);
2830         log.debug(matched, 'vms: finish');
2831         return callback(null, matched);
2832     });
2833 }
2834 
2835 
2836 /**
2837  * Gets rules that apply to a Remote VM
2838  *
2839  * @param opts {Object} : options:
2840  * - vms {Array} : array of VM objects (as per VM.js)
2841  * - vm {UUID} : UUID of VM to get the rules for
2842  * @param callback {Function} `function (err, rules)`
2843  * - Where rules is an array of rules that apply to the VM
2844  */
2845 function getRemoteVMrules(opts, callback) {
2846     try {
2847         assert.object(opts, 'opts');
2848         assertStringOrObject(opts.remoteVM, 'opts.remoteVM');
2849         assert.arrayOfObject(opts.vms, 'opts.vms');
2850     } catch (err) {
2851         return callback(err);
2852     }
2853     var log = util_log.entry(opts, 'rvmRules', true);
2854 
2855     pipeline({
2856     funcs: [
2857         function lock(_, cb) {
2858             mod_lock.acquireSharedLock(cb);
2859         },
2860         function allRules(_, cb) { loadAllRules(log, cb); },
2861         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2862         function rvm(_, cb) {
2863             if (typeof (opts.remoteVM) === 'object') {
2864                 return cb(null, opts.remoteVM);
2865             }
2866 
2867             return mod_rvm.load(opts.remoteVM, log, cb);
2868         },
2869         function rvms(state, cb) {
2870             mod_rvm.create({ allVMs: state.vms, requireIPs: false, log: log },
2871                 [ state.rvm ], function (e, rvmList) {
2872                 if (e) {
2873                     return cb(e);
2874                 }
2875 
2876                 createRemoteVMlookup(rvmList, log, cb);
2877             });
2878         },
2879         function rvmRules(state, cb) {
2880             filter.rulesByRVMs(state.rvms, state.allRules, log, cb);
2881         }
2882     ]}, function (err, res) {
2883         mod_lock.releaseLock(res.state.lock);
2884 
2885         if (err) {
2886             util_log.finishErr(log, err, 'rvmRules: finish (vm=%s)',
2887                 opts.remoteVM);
2888             return callback(err);
2889         }
2890 
2891         var toReturn = res.state.rvmRules.map(function (r) {
2892             return r.serialize();
2893         });
2894 
2895         log.debug(toReturn, 'rvmRules: finish (vm=%s)', opts.remoteVM);
2896         return callback(null, toReturn);
2897     });
2898 }
2899 
2900 
2901 /**
2902  * Gets rules that apply to a VM
2903  *
2904  * @param opts {Object} : options:
2905  * - vms {Array} : array of VM objects (as per VM.js)
2906  * - vm {UUID} : UUID of VM to get the rules for
2907  * @param callback {Function} `function (err, rules)`
2908  * - Where rules is an array of rules that apply to the VM
2909  */
2910 function getVMrules(opts, callback) {
2911     try {
2912         assert.object(opts, 'opts');
2913         assert.string(opts.vm, 'opts.vm');
2914         assert.arrayOfObject(opts.vms, 'opts.vms');
2915     } catch (err) {
2916         return callback(err);
2917     }
2918     var log = util_log.entry(opts, 'vmRules', true);
2919 
2920     var toFind = {};
2921     toFind[opts.vm] = opts.vm;
2922 
2923     pipeline({
2924     funcs: [
2925         function lock(_, cb) {
2926             mod_lock.acquireSharedLock(cb);
2927         },
2928         function allRules(_, cb) { loadAllRules(log, cb); },
2929         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2930         function vmRules(state, cb) {
2931             filter.rulesByVMs(state.vms, toFind, state.allRules, log, cb);
2932         }
2933     ]}, function (err, res) {
2934         mod_lock.releaseLock(res.state.lock);
2935 
2936         if (err) {
2937             util_log.finishErr(log, err, 'vmRules: finish (vm=%s)', opts.vm);
2938             return callback(err);
2939         }
2940 
2941         var toReturn = res.state.vmRules.map(function (r) {
2942             return r.serialize();
2943         });
2944 
2945         log.debug(toReturn, 'vmRules: finish (vm=%s)', opts.vm);
2946         return callback(null, toReturn);
2947     });
2948 }
2949 
2950 
2951 /**
2952  * Validates an add / update payload
2953  *
2954  * @param opts {Object} : options:
2955  *   - localVMs {Array} : list of local VMs
2956  *   - remoteVMs {Array} : list of remote VMs
2957  *   - rules {Array} : list of rules
2958  *   - vms {Array} : array of VM objects (as per VM.js)
2959  * @param callback {Function} `function (err, rules)`
2960  * - Where rules is an array of rules that apply to the VM
2961  */
2962 function validatePayload(opts, callback) {
2963     try {
2964         assert.object(opts, 'opts');
2965         assert.arrayOfObject(opts.vms, 'opts.vms');
2966         assert.optionalArrayOfObject(opts.rules, 'opts.rules');
2967         assert.optionalArrayOfObject(opts.localVMs, 'opts.localVMs');
2968         assert.optionalArrayOfObject(opts.remoteVMs, 'opts.remoteVMs');
2969 
2970         var optRules = opts.rules || [];
2971         var optLocalVMs = opts.localVMs || [];
2972         var optRemoteVMs = opts.remoteVMs || [];
2973         if (optRules.length === 0 && optLocalVMs.length === 0
2974             && optRemoteVMs.length === 0) {
2975             throw new Error(
2976                 'Payload must contain one of: rules, localVMs, remoteVMs');
2977         }
2978     } catch (err) {
2979         return callback(err);
2980     }
2981     var log = util_log.entry(opts, 'validatePayload');
2982 
2983     pipeline({
2984     funcs: [
2985         function lock(_, cb) {
2986             mod_lock.acquireSharedLock(cb);
2987         },
2988         function rules(_, cb) {
2989             createRules(opts.rules, cb);
2990         },
2991         function vms(_, cb) { createVMlookup(opts.vms, log, cb); },
2992         function remoteVMs(_, cb) { mod_rvm.loadAll(log, cb); },
2993         function newRemoteVMs(state, cb) {
2994             mod_rvm.create({ allVMs: state.vms, requireIPs: true, log: log },
2995                 opts.remoteVMs, cb);
2996         },
2997         // Create a combined remote VM lookup of remote VMs on disk plus
2998         // new remote VMs in the payload
2999         function allRemoteVMs(state, cb) {
3000             createRemoteVMlookup([state.remoteVMs, state.newRemoteVMs],
3001                 log, cb);
3002         },
3003 
3004         function validate(state, cb) {
3005             validateRules(state.vms, state.allRemoteVMs, state.rules, log, cb);
3006         }
3007     ]}, function (err, res) {
3008         mod_lock.releaseLock(res.state.lock);
3009 
3010         if (err) {
3011             util_log.finishErr(log, err, 'validatePayload: finish');
3012             return callback(err);
3013         }
3014 
3015         log.debug(opts.payload, 'validatePayload: finish');
3016         return callback();
3017     });
3018 }
3019 
3020 
3021 
3022 module.exports = {
3023     _setOldIPF: mod_ipf._setOld,
3024     add: add,
3025     del: del,
3026     disable: disableVM,
3027     enable: enableVM,
3028     get: getRule,
3029     getRVM: getRemoteVM,
3030     list: listRules,
3031     listRVMs: listRemoteVMs,
3032     remoteTargets: getRemoteTargets,
3033     rvmRules: getRemoteVMrules,
3034     setBunyan: util_log.setBunyan,
3035     stats: vmStats,
3036     status: vmStatus,
3037     update: update,
3038     validatePayload: validatePayload,
3039     VM_FIELDS: VM_FIELDS,
3040     vmRules: getVMrules,
3041     vms: getRuleVMs
3042 };