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