zcage
Version:
Zone administration API for Illumos
1,482 lines (1,324 loc) • 114 kB
JavaScript
/* zone managing functions for zcage illumos zone manager
* Copyright (c) 2018, Carlos Neira cneirabustos@gmail.com
*
* This file is part of Zcage.
*
* zcage is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* zcage is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with zcage. If not, see <http://www.gnu.org/licenses/>.
*/
const ip = require('ipaddr.js');
const chalk = require('chalk');
const bytes = require('bytes');
const uuidv4 = require('uuid/v4');
var fs = require('fs'),
xml2js = require('xml2js');
const util = require('util');
const VError = require('verror');
const dockerNames = require('docker-names');
var columnify = require('columnify');
const {
spawnSync,
spawn,
fork
} = require('child_process');
const zfs = require('./imgadm');
var Ajv = require('ajv');
var validator = require('validator');
const path = require('path');
var debug = {
enabled: false
};
const ubuntu_distros = ['xenial', 'disco', 'bionic'];
function mapToObj(inputMap) {
let obj = {};
inputMap.forEach(function(value, key) {
obj[key] = value;
});
return obj;
}
function ZonesString() {
var zoneadm = spawnSync('zoneadm', ['list', '-cp']);
if (!zoneadm.stdout) {
console.log(
"You must be root or use an account with Primary Administrator Role to use zcage"
);
return process.exit();
}
var zones = zoneadm.stdout.toString().split('\n');
zones.pop();
return zones;
}
function Zonedata(zonestring) {
var values = zonestring.toString().split(':');
var keys = ["zoneid", "zonename", "state", "zone-path", "uuid", "brand",
"ip-type"
];
var m = new Map();
values.pop();
for (var i = 0, len = values.length; i < len; i++) {
m.set(keys[i], values[i]);
}
return mapToObj(m);
}
function list() {
var zones = ZonesString();
var zoneobjs = [];
for (var i = 0, len = zones.length; i < len; i++) {
zoneobjs.push(Zonedata(zones[i]));
}
return zoneobjs;
}
function listzones(listOptions, out) {
var zones = ZonesString();
var zoneobjs = [];
var type;
for (var i = 0, len = zones.length; i < len; i++) {
z = Zonedata(zones[i]);
if (listOptions && ("state" in listOptions)) {
if (z.state != "configured" && z.state == "installed")
z.state = "stopped";
if (listOptions && listOptions.state == z.state && z.zonename != "global")
zoneobjs.push(z);
} else {
if (z.state == "incomplete")
continue;
if ("configured" != z.state) {
switch (z.state) {
case "installed":
z.state = "stopped";
break;
}
if (z.zonename != "global")
zoneobjs.push(z);
}
}
}
if (!out) {
var data = [];
console.log(
"UUID\t\t\t\t TYPE STATE RAM \tALIAS\t\t\t CREATED"
);
for (i = 0, len = zoneobjs.length; i < len; i++) {
if (zoneobjs[i].zonename != "global") {
getzonedata(zoneobjs[i].zonename, function(zdata, z) {
if (zdata.name == 'incomplete')
return;
let ram;
let ipaddr = "unknown";
ram = zdata.ram;
let created;
if ("CreatedAt" in zdata)
created = {
CreatedAt: zdata['CreatedAt']
};
else
created = {
CreatedAt: ""
};
switch (zdata.brand) {
case "lx":
type = "LX";
break;
case "bhyve":
type = "BHYVE";
break;
case "kvm":
type = "KVM";
break;
case "dpkg":
type = "DPKG";
break;
default:
type = "OS";
break;
}
data = [{
UUID: z.uuid,
TYPE: type,
STATE: z.state,
RAM: ram,
NAME: z.zonename,
CREATED: created.CreatedAt
}];
var columns = columnify(data, {
showHeaders: false,
Align: 'right',
config: {
TYPE: {
minWidth: 6
},
STATE: {
minWidth: 8
},
RAM: {
minWidth: 10
},
NAME: {
minWidth: 21
},
}
});
console.log(columns);
}, zoneobjs[i]);
}
}
}
return zoneobjs;
}
function getdata(zonename, uuid) {
var zones = list();
var zone = zones.filter(zone => (zone.zonename == zonename || zone.uuid ==
uuid));
if (zone == undefined || zone.length == 0)
return null;
return zone[0];
}
function destroy(zonename, dzvol, callback) {
if (!isAbletoexec()) {
console.log(
"You must be root or use an account with Primary Administrator Role to Activate zcage (pfexec zcage activate)"
);
return null;
}
var zoption = '-z';
var z = '';
if (validator.isUUID(zonename)) {
z = getdata(zonename, zonename);
zoption = '-u';
} else {
z = getdata(zonename);
}
var zdestroy;
if (z == null) {
console.log(chalk `Destroying zone: ${zonename} [{red.bold ERR}] `);
console.log(`${zonename} does not exists`);
if (callback) {
callback(404, {
message: "No such container: " + zonename
});
}
return null;
}
if (z.state == "running") {
var zhalt = spawnSync('pfexec', ['zoneadm', zoption, zonename,
'halt'
]);
var errorText = zhalt.stderr.toString().trim();
if (errorText) {
console.log(chalk `Halting zone: ${zonename} [{red.bold ERR}] `);
console.log(errorText);
if (callback) {
callback(500, {
message: errorText
});
}
return zhalt.status;
}
}
zdestroy = spawnSync('pfexec', ['zoneadm', zoption, zonename, 'uninstall',
'-F'
]);
var errorText = zdestroy.stderr.toString().trim();
if (errorText) {
console.log(chalk `Uninstalling zone: ${zonename} [{red.bold ERR}] `);
console.log(errorText);
if (callback) {
callback(500, {
message: errorText
});
}
}
if (dzvol == true && (z.brand == 'bhyve' || z.brand == 'kvm')) {
getzonedata(z.zonename, function(zdata) {
var obj = zdata.vm_data;
var zvol = obj.bootdisk;
if (zvol) {
let zvol_destroy = spawnSync('pfexec', ['zfs',
'destroy', zvol
]);
var errorText = zvol_destroy.stderr.toString().trim();
if (errorText) {
console.log(errorText);
if (callback) {
callback(500, {
message: errorText
});
}
return zvol_destroy.status;
}
}
});
}
zdestroy = spawnSync('pfexec', ['zonecfg', '-z', z.zonename, 'delete',
'-F'
]);
var errorText = zdestroy.stderr.toString().trim();
if (errorText) {
console.log(chalk `Deleting zone: ${zonename} [{red.bold ERR}] `);
console.log(errorText);
if (callback) {
callback(500, {
message: errorText
});
}
}
console.log(chalk `${zonename} destroyed [{green.bold OK}] `);
if (callback) {
callback(204, {
message: "Container Id: " + zonename + " destroyed"
});
}
return zdestroy.status;
}
function getinfo(zonename) {
var zonecfg = spawnSync('zonecfg', ['-z', zonename, 'info']);
var errorText = zonecfg.stderr.toString().trim();
if (errorText)
console.log("info " + errorText);
return zonecfg.stdout.toString();
}
function exec(zonename, cmd, out) {
var zlogin = spawnSync('pfexec', ['/usr/sbin/zlogin', zonename, cmd]);
var errorText = zlogin.stderr.toString().trim();
if (errorText)
console.log("error executing " + cmd + " error: " + errorText);
if (out != undefined)
out[cmd] = zlogin.stdout.toString();
return zlogin.status;
}
function fixup_dataset(zonename, args, out) {
var fixup = spawnSync('pfexec', args, {
shell: true
});
var errorText = fixup.stderr.toString().trim();
if (errorText)
console.log("error executing pfexec error: " + errorText);
return fixup;
}
function start(zonename, opts, callback, quiet) {
if (!isAbletoexec()) {
console.log(
"You must be root or use an account with Primary Administrator Role to Activate execute this action"
);
return null;
}
var zoption = '-z';
var zone;
zonename += '';
if (validator.isUUID(zonename)) {
zone = getdata(zonename, zonename);
zoption = '-u';
} else {
zone = getdata(zonename);
}
if (zone == null) {
console.log(chalk `{red.bold Error : alias does not exists}`);
if (callback) {
callback(404, {
message: "No such container: " + zonename
});
}
return null;
}
if (zone.state == "running") {
if (callback) {
callback(304, {
message: "Container already started."
});
}
console.log(chalk `{blue.bold vm uuid ${zone.uuid} is already running}`);
return null;
}
if (vnicused(zone.zonename)) {
console.log(chalk `zone: ${zonename} start [{red.bold ERR}] `);
console.log("vnic is being used by other container");
if (callback) {
callback(500, {
message: "vnic is being used by other container"
});
}
return null;
}
if ((zone.brand == 'bhyve' || zone.brand == 'kvm') && (opts != undefined &&
opts != null)) {
var script = "add fs; set dir=" + "\"" +
opts + "\"" +
";set special=" + "\"" +
opts + "\"" +
"; set type=lofs;add options ro;" +
"add options nodevices;end;";
var r = spawnSync('pfexec', ['zonecfg', zoption, zonename,
'add attr; set type=string;set name=cdrom;set value=' + opts + ";end;" +
script
]);
if (debug.enabled)
console.log(r.stderr.toString(), r.stdout.toString());
}
var start = spawnSync('pfexec', ['zoneadm', zoption, zonename, 'boot']);
var errorText = start.stderr.toString().trim();
if (errorText && zone.brand != 'dpkg') {
console.log(chalk `${zonename} start [{red.bold ERR}] `);
if (callback) {
callback(500, {
message: errorText
});
}
} else {
if (!quiet)
console.log(chalk `${zonename} started [{green.bold OK}] `);
if (callback) {
callback(204, {
message: "Container Id:" + zone.uuid + "started"
});
}
}
if (zone.brand == 'bhyve' || zone.brand == 'kvm') {
isvnc_enabled(zone, zone.brand);
}
getzonedata(zonename, function(zdata) {
if (zdata.Cmd) {
var logs = zdata.Cmd.replace(',', " ") + ' 2> ' +
'/stderr.log' + ' 1> ' + '/stdout.log';
var Cmd = ['zlogin', zdata.name, logs];
var execmd = spawnSync('pfexec', Cmd);
if (debug.enabled) {
console.log("executing command - stdout: " +
execmd.stdout.toString() +
" stderr: " + execmd.stderr.toString());
}
}
});
if (validator.isUUID(zonename)) {
zone = getdata(zonename, zonename);
zoption = '-u';
} else {
zone = getdata(zonename);
}
zone_status = 'off';
while (zone_status != 'running') {
if (validator.isUUID(zonename)) {
zone = getdata(zonename, zonename);
zoption = '-u';
} else {
zone = getdata(zonename);
}
zone_status = zone.state;
}
if (zone.brand == 'lx') {
// Execute any entrypoint a docker image may have.
run_container_config(zonename, 'container-config');
}
if (debug.enabled)
console.log("starting zone", start.stderr.toString(), start.stdout.toString());
return start.status;
}
function rctl(zname, rctlOptions, callback) {
if (!isAbletoexec()) {
console.log(
"You must be root or use an account with Primary Administrator Role to Activate execute this action"
);
return null;
}
var rctlobj = {};
var z;
if (validator.isUUID(zname)) {
z = getdata(zname, zname);
} else {
z = getdata(zname);
}
if (z == null) {
console.log(chalk `{red.bold Error : Alias does not exists}`);
return null;
}
delete rctlOptions.zonename;
rctlobj.rctl = addrctl(rctlOptions, z.zonename);
update_rctl(z.zonename, rctlobj.rctl, function(err, zname, script, callback) {
Asynczonecfgexec(err, zname, script, callback);
}, callback);
}
function Asynczonecfgexec(err, zname, script, callback) {
if (err) {
console.log("error on creating script", err);
if (callback)
callback(400, {
message: "Something wrong happened"
});
return;
}
var zonecfg = spawnSync('pfexec', ['zonecfg', '-z', zname, script]);
if (zonecfg.err) {
console.log("error on zonecfg executing script", zonecfg.stderr.toString());
if (callback)
callback(400, {
message: "Something wrong happened"
});
return;
}
console.log(zonecfg.stderr.toString());
console.log(zonecfg.stdout.toString());
if (callback) {
callback(200, {
Warnings: []
});
}
return zonecfg.status;
}
function halt(zonename, callback, quiet) {
if (!isAbletoexec()) {
console.log(
"You must be root or use an account with Primary Administrator Role to Activate execute this action"
);
return null;
}
var zoption = '-z';
var z = '';
zonename += '';
if (validator.isUUID(zonename)) {
z = getdata(zonename, zonename);
zoption = '-u';
} else {
z = getdata(zonename);
}
if (z == null) {
if (callback) {
callback(404, {
message: "No such container: " + z.uuid
});
}
console.log("Error zone does not exists");
return null;
}
if (z != null && (z.brand == 'bhyve' || z.brand == 'kvm')) {
spawnSync('pfexec', ['zonecfg', zoption, zonename,
'remove attr name=cdrom; remove fs;'
]);
}
var stop = spawnSync('pfexec', ['zoneadm', zoption, zonename, 'halt']);
var errorText = stop.stderr.toString().trim();
if (errorText && callback) {
callback(500, {
message: errorText
});
console.log("info ", stop.stdout.toString());
return;
} else {
if (callback) {
callback(204, {
message: "Container Id:" + z.uuid + "stopped"
});
return null;
}
if (!quiet)
console.log(chalk `${zonename} stopped [{green.bold OK}] `);
}
return stop.stdout.toString();
}
function reboot(zonename, callback) {
if (!isAbletoexec()) {
console.log(
"You must be root or use an account with Primary Administrator Role to Activate execute this action"
);
return null;
}
var zoption = '-z';
var z;
var reboot;
if (validator.isUUID(zonename)) {
z = getdata(zonename, zonename);
} else {
z = getdata(zonename);
}
if (z == null) {
console.log("Error zone does not exists");
if (callback) {
callback(404, {
message: "No such container: " + zonename
});
}
return;
}
if (z.state == "running") {
if (z.brand == 'bhyve' || z.brand == 'kvm') {
var path = z['zone-path'] + '/root/tmp';
var vncpid = spawnSync('pgrep', ['-f', path]);
var pid = vncpid.stdout.toString();
if (vncpid.error != null) {
console.log(`Error restarting vnc`);
}
vncpid = spawnSync('pfexec', ['kill', '-9', pid]);
if (vncpid.error != null) {
console.log(`Error killing vnc pid : {pid}`);
}
}
reboot = spawnSync('pfexec', ['zoneadm', zoption, zonename,
'reboot'
]);
var errorText = reboot.stderr.toString().trim();
if (errorText) {
console.log("info ", errorText);
if (callback) {
callback(500, {
message: errorText
});
}
} else {
console.log(chalk `zone: ${zonename} rebooted [{green.bold OK}] `);
if (callback) {
callback(204, {
message: "Container Id:" + z.uuid + " Restarted"
});
return reboot.status;
}
}
return reboot.status;
} else {
console.log("Error vm is not running");
if (callback) {
callback(204, {
message: "Container Id:" + z.uuid + "Already running"
});
return reboot.status;
}
}
}
function install(zonename, brand, uuid, zone_spec) {
var iz;
if (brand == 'lx' || brand == 'illumos') {
var img;
if (brand != 'illumos' && zone_spec.docker) {
var tags = zone_spec.docker;
if (tags.length != 2) {
console.log("docker tag setup incorrectly: should be for example library/alpine latest " + zone_spec.docker);
return -1;
}
debug.enabled && console.log("docker tags installing ", tags);
img = zfs.docker_pull(tags[0], tags[1]);
if (debug.enabled) {
console.log("docker image pulled: " + img);
}
//DILOS: must move omnios brands(5) to dilos.
iz = spawnSync('pfexec', ['zoneadm', '-z', zonename, 'install', '-t',
img
], {
shell: true
});
// At this point the zone should be in stated configured/installed
zone_status = '';
while (zone_status != "installed") {
if (validator.isUUID(zonename)) {
z = getdata(zonename, zonename);
} else {
z = getdata(zonename);
}
zone_status = z.state
}
} else {
let option = '-s';
if (validator.isUUID(uuid)) {
img = zfs.ZCAGE.IMAGES + '/' + uuid + '.zss.gz';
} else {
img = zfs.ZCAGE.IMAGES + '/' + uuid;
option = '-t';
}
iz = spawnSync('pfexec', ['zoneadm', '-z', zonename, 'install', option,
img
], {
shell: true
});
}
var errorText = iz.stderr.toString().trim();
if (errorText) {
console.log("Error installing " + errorText);
return iz.status;
}
} else {
if (zone_spec && zone_spec['with-image'] && brand != 'kvm' && brand != 'bhyve') {
if (debug.enabled)
console.log("installing ", zonename);
let option = '-s';
if (validator.isUUID(uuid)) {
img = zfs.ZCAGE.IMAGES + '/' + uuid + '.zss.gz';
} else {
img = zfs.ZCAGE.IMAGES + '/' + uuid;
option = '-t';
}
if (debug.enabled)
console.log("installing img", img);
iz = spawnSync('pfexec', ['zoneadm', '-z', zonename, 'install', option,
img
], {
shell: true
});
var errorText = iz.stderr.toString().trim();
if (errorText) {
console.log("Error installing " + errorText);
return iz.status;
}
} else {
if (debug.enabled)
console.log("installing ", zonename);
if (brand == "dpkg")
console.log("Installation of dpkg zone will take a couple of minutes...", zonename);
iz = spawnSync('pfexec', ['zoneadm', '-z', zonename, 'install']);
var errorText = iz.stderr.toString().trim();
if (errorText) {
console.log("Error installing ", iz.stderr.toString(),
iz.stdout.toString());
return iz.status;
}
}
}
if (debug.enabled)
console.log("zoneadm: install returned ", iz.stderr.toString(), iz.stdout.toString());
if ((brand == 'bhyve' || brand == 'kvm') && zone_spec['with-image']) {
img = zfs.ZCAGE.IMAGES + '/' + uuid;
if (zone_spec['with-image'].toUpperCase().indexOf("CLOUD") > -1) {
zone_spec['uses-cloud-init'] = true;
/* Ubuntu does not use eth0 */
zone_spec.cloud_nic = 'eth0';
ubuntu_distros.forEach(function(d) {
if (zone_spec['with-image'].includes(d)) {
zone_spec.cloud_nic = 'enp0s6';
}
})
}
if (debug.enabled) {
console.log("vm setting image", JSON.stringify(zone_spec, null, 4));
console.log("vm image used for zvol creation: " + img);
console.log("vm zvol size = " + zone_spec.rctl.quota);
console.log(" is cloud init = " + zone_spec['uses-cloud-init']);
}
iz = spawnSync('pfexec', ['bhyve-zvol.sh', img, zone_spec.rctl.quota,
zone_spec.vm_metadata['disk']
]);
var errorText = iz.stderr.toString().trim();
if (debug.enabled)
console.log("bhyve-zvol stdout:" + iz.stdout.toString() +
"stderr: " + iz.stderr.toString());
if (errorText) {
console.log("Converting to raw " + errorText);
return iz.status;
}
if (zone_spec['uses-cloud-init'] == true) {
if (!zone_spec.udata) {
console.log("This seems to be a cloud-init image," +
" but --udata was not provided\nNo setup being done");
} else {
iz = spawnSync('pfexec', ['cloud-init.sh', JSON.stringify(zone_spec, null,
4)]);
if (debug.enabled)
console.log("cloud-init stdout:" + iz.stdout.toString() +
"stderr: " + iz.stderr.toString());
var errorText = iz.stderr.toString().trim();
if (errorText) {
console.log("Setting up cloud-init data " + errorText);
return iz.status;
}
}
}
}
return iz.status;
}
function uninstall(zonename) {
var uz = spawnSync('pfexec', ['zoneadm', '-z', zonename, 'uninstall']);
if (uz.error != null)
console.log("Error uninstalling ", uz.stderr.toString());
return uz.status;
}
function create(zonename, zone_spec) {
var script = spec2script(zone_spec);
if (debug.enabled) {
console.log("zone_spec:", JSON.stringify(zone_spec, null, 4));
console.log("spec2script:", script);
}
var status = -1;
if (script != null) {
var zonecfg = spawnSync('pfexec', ['zonecfg', '-z', zonename, script]);
if (debug.enabled)
console.log("configuring zone", zonecfg.stdout.toString(),
zonecfg.stderr.toString());
if (zonecfg.err != null) {
console.log("Error creating", zonecfg.stderr.toString());
return null;
}
if (zone_spec.brand == 'lx' || zone_spec.brand == 'bhyve' || zone_spec.brand ==
'kvm' || zone_spec.brand == 'illumos') {
status = install(zonename, zone_spec.brand, zone_spec['with-image'],
zone_spec);
if (status != 0) {
console.log("Error: Installing zone", zonename);
return null;
}
} else {
status = install(zonename, zone_spec.brand);
if (status != 0) {
console.log("Error: Installing zone", zonename);
return null;
}
}
if (zone_spec.brand != 'lx' && zone_spec.brand != 'bhyve' && zone_spec.brand !=
'kvm') {
status = setupzone(zonename, zone_spec);
if (status != 0) {
console.log("Zone postsetup failed= ", status);
return status;
}
halt(zonename, null, true);
}
if ("quota" in zone_spec.rctl) {
var cmd = "quota=" + zone_spec.quota + " ";
var ds = zfs.GetPool() + zone_spec.zonepath;
if (debug.enabled)
console.log("Setting quota=%s on dataset %s", zone_spec.rctl.quota, ds);
var quota = spawnSync('pfexec', ['zfs', 'set', 'quota=' +
zone_spec.rctl.quota, ds
]);
if (debug.enabled)
console.log("setting zfs quota", quota.stderr.toString());
}
}
return status;
}
function setupzone(zonename, zone_spec) {
var status = 0;
var zone_status = ' ';
switch (zone_spec.brand) {
case "sparse":
case "pkgsrc":
status = start(zonename, null, null, false);
if (status != 0) {
console.log("Error: Starting zone", zonename);
}
while (zone_status != "running") {
if (validator.isUUID(zonename)) {
z = getdata(zonename, zonename);
} else {
z = getdata(zonename);
}
zone_status = z.state
}
/* zone must be up to take commands */
var setup = genpostscript(zone_spec);
status = exec(zonename,
"svcs svc:/milestone/multi-user | grep online");
while (status != 0) {
status = exec(zonename,
"svcs svc:/milestone/multi-user | grep online");
}
exec(zonename, setup);
break;
case "illumos":
zone_status = "installing";
while (zone_status != "installed") {
if (validator.isUUID(zonename)) {
z = getdata(zonename, zonename);
} else {
z = getdata(zonename);
}
zone_status = z.state
}
postshellcmd = ['rm', '-rf', z['zone-path'] + "/root/ccs"];
out = fixup_dataset(zonename, postshellcmd);
if (out.status != 0)
return process.exit();
postshellcmd = ['rm', '-rf', z['zone-path'] + "/root/config"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['rm', '-rf', z['zone-path'] + "/root/cores"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['rm', '-rf', z['zone-path'] + "/root/dev"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['rm', '-rf', z['zone-path'] + "/root/local"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['rm', '-rf', z['zone-path'] + "/root/site"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['rm', '-rf', z['zone-path'] + "/root/lastbooted"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['mv', z['zone-path'] + "/root/root", z['zone-path'] + "/root/old"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['mv', z['zone-path'] + "/root/old/*", z['zone-path'] + "/root/"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['rm', '-rf', z['zone-path'] + "/root/old"];
fixup_dataset(zonename, postshellcmd);
postshellcmd = ['chmod', '755', z['zone-path'] + "/*"];
fixup_dataset(zonename, postshellcmd);
status = start(zonename, null, null, false);
if (status != 0) {
console.log("Error: Starting zone", zonename);
}
while (zone_status != "running") {
if (validator.isUUID(zonename)) {
z = getdata(zonename, zonename);
} else {
z = getdata(zonename);
}
zone_status = z.state
}
var setup = genpostscript(zone_spec);
/* ipadm must be online to create interfaces */
status = exec(zonename,
"svcs svc:/network/ip-interface-management:default | grep online", out);
while (status != 0) {
status = exec(zonename,
"svcs svc:/network/ip-interface-management:default | grep online", out);
}
exec(zonename, setup);
break;
default:
break;
}
return status;
}
function create_zone_spec(resources) {
var spec = {
zonepath: "",
brand: "",
'ip-type': "exclusive",
'dns-domain': "",
resolvers: ["8.8.8.8", "8.8.8.4"],
autoboot: false
};
if (resources != null && ("net", "brand" in resources)) {
Object.keys(resources).forEach(function(key) {
spec[key] = resources[key];
});
}
if (spec.brand === "bhyve" || spec.brand === "kvm") {
var vm_metadata = {
acpi: 'on',
netif: 'virtio-net-viona',
type: 'generic',
vcpus: "1",
ram: "2G"
};
if (spec.brand === "bhyve") {
vm_metadata.bootrom = "BHYVE";
vm_metadata.hostbridge = "intel";
vm_metadata.vnc = "unix=/tmp/" + uuidv4() + ".vnc";
}
if (spec.brand === "kvm") {
vm_metadata.vnc = "on";
if (debug.enabled) {
console.log("zone spec ram: " + spec.ram + "vm_metadata :" + vm_metadata.ram);
}
if (resources.vm_metadata.ram.includes('gb')) {
resources.vm_metadata.ram = resources.vm_metadata.ram.replace('gb', 'G');
} else if (resources.vm_metadata.ram.includes('mb')) {
resources.vm_metadata.ram = resources.vm_metadata.ram.replace('mb', 'M');
}
if (debug.enabled) {
console.log("kvm changing gb|mb to G|M current ram: " + spec.ram +
"vm_metadata :" + vm_metadata.ram);
}
}
spec.vm_metadata = vm_metadata;
Object.keys(resources.vm_metadata).forEach(function(key) {
if (resources.vm_metadata[key] != undefined) {
spec.vm_metadata[key] = resources.vm_metadata[key];
}
});
}
if (debug.enabled) {
console.log("spec", JSON.stringify(spec, null, 4));
console.log("VM", JSON.stringify(spec.vm_metadata, null, 4));
}
return spec;
}
function spec2script(spec) {
var script = "";
script = "create;";
if (spec.brand == 'lx') {
script +=
"add attr;set name=kernel-version;set type=string;set value=3.16.0;end;";
}
if (spec.brand == 'illumos') {
script += "add fs; set dir=" + "/usr" +
";set special=" + "/usr" +
"; set type=lofs;add options ro;end;"
script += "add fs; set dir=" + "/sbin" +
";set special=" + "/sbin" +
"; set type=lofs;add options ro;end;"
script += "add fs; set dir=" + "/lib" +
";set special=" + "/lib" +
"; set type=lofs;add options ro;end;"
}
Object.keys(spec).forEach(function(key) {
switch (key) {
case "net":
spec.net.forEach(function(e) {
script +=
" add net ;";
if (e.address == 'dhcp') {
script += "set physical=" + e.physical + ";";
if (spec.brand == 'lx') {
script += "add property (name=ip,value=\"dhcp\");";
script += "add property (name=primary,value=\"true\");";
script += "add property (name=ips,value=\"dhcp\");";
script += "add property (name=primary,value=\"true\");";
}
script += " end;";
} else {
script += "set physical=" + e.physical + ";";
var prefix = ip.IPv4.parse(e.netmask.toString())
.prefixLengthFromSubnetMask();
var addr = e.address + "/" + prefix +
";";
script += "set allowed-address=" + addr +
";";
script += "set defrouter=" + e.gateway +
";";
script += " end;";
if (spec.brand == 'illumos') {
postshellcmd = "\"ipadm create-if " + e.physical + ";";
prefix = ip.IPv4.parse(e.netmask).prefixLengthFromSubnetMask()
postshellcmd += "ipadm create-addr -t -T static -a local=" + e.address + "/" + prefix + " " + e.physical + "/v4 " + ";";
postshellcmd += " route add default " + e.gateway + "\"";
script += "add attr;set name=ENTRYPOINT; set type=string; set value=" + postshellcmd + ";end;";
}
}
});
break;
case "resolvers":
script +=
"add attr;set name=resolvers;set type=string;set value=";
for (var i = 0, len = spec.resolvers.length; i < len; i++) {
if (i + 1 >= len)
script += spec.resolvers[i] + ";";
else
script += spec.resolvers[i] + ",";
}
script += "end;";
break;
case "dns-domain":
break;
case "rctl":
if ("dedicated-cpu" in spec.rctl) {
script += "add dedicated-cpu;set ncpus=" +
spec.rctl["dedicated-cpu"] + ";";
if ("importance" in spec.rctl) {
script += "set importance=" + spec.rctl.importance +
";";
}
script += "end; ";
}
Object.keys(spec.rctl).forEach(function(key) {
if (key == 'quota') {
script +=
"add attr; set name=quota;set type=string;" +
" set value=" + spec.rctl.quota +
";end;";
} else
if (key == "max-physical-memory" ||
key == "max-locked-memory" ||
key == "max-swap" ||
key == "max-lwps" ||
key == "max-shm-ids" ||
key == "max-msg-ids" ||
key == "max-shm-memory") {
script += "add rctl;";
script += "set name=zone." + key + ";" +
"add value (priv=privileged,limit=" +
spec.rctl[key] + ",action=deny);";
script += "end; ";
} else if (key == "cpu-shares") {
script += "set cpu-shares=" + spec.rctl["cpu-shares"] + ";"
}
});
break;
case "vm_metadata":
Object.keys(spec.vm_metadata).forEach(function(key) {
switch (key) {
case 'vnc':
script +=
"add attr; set type=string;set name=" +
key + "; set value=" +
"\"" + spec.vm_metadata[key] +
"\"" +
" ;end;";
script +=
"add attr; set type=string;" +
"set name=vnc-port;" +
"set value=" +
(Math.floor(Math.random() *
(5999 - 5900) + 5900)).toString() +
";end;;";
break;
case 'ram':
case 'bootrom':
case 'console':
case 'hostbridge':
case 'cdrom':
case 'vcpus':
case 'bootorder':
case 'bootdisk':
script +=
"add attr; set type=string;set name=" +
key + "; set value=" +
"\"" + spec.vm_metadata[key] +
"\"" +
" ;end;";
break;
case 'disk':
script += "add device; set match=" +
"/dev/zvol/rdsk/" +
spec.vm_metadata.bootdisk +
" ; end;";
break;
case 'fs':
script += "add fs; set dir=" + "\"" +
spec.vm_metadata[key] + "\"" +
";set special=" + "\"" +
spec.vm_metadata[key] + "\"" +
"; set type=lofs;add options ro;" +
"add options nodevices;end;";
break;
case 'device':
script += "add device; set match=" +
spec.vm_metadata[key] +
";end;";
break;
}
});
break;
default:
if (key != 'with-image' && key != 'alias' && key != 'debug' && key !=
'docker' && key != 'udata' && key != 'cpu' && key != 'image' && key != "Cmd")
script += " set " + key + "=" + spec[key] + ";";
break;
}
});
script += ";verify; commit;";
if (debug.enabled)
console.log(arguments.callee.name, script.split(';'));
return script;
}
/*
* This is needed to setup networking in sparse zones
* # zoneadm -z omni boot
* # zlogin omni
* # ipadm create-if omni0
* # ipadm create-addr -T static -a local=x.x.x.x/y omni0/v4
* # echo x.x.x.x > /etc/defaultrouter
* # echo nameserver 80.80.80.80 > /etc/resolv.conf
* # cp /etc/nsswitch.{dns,conf}
* # svcadm restart routing-setup
* For illumos branded zones, we need to :
* - Add default route : route add default <gatewayip>
* - ipadm create-if <physical nic>
* - ipadm create-addr -t -T static -a local=1.2.3.4/24 tt0/v4
*/
function genpostscript(zone_spec) {
var postshellcmd = "";
switch (zone_spec.brand) {
case "sparse":
case "pkgsrc":
case "lipkg":
case "ipkg":
case "bhyve":
case "kvm":
case "illumos":
postshellcmd += " sleep 1 ;";
var resolvers = zone_spec.resolvers.toString().split(",");
for (var i = 0, len = resolvers.length; i < len; i++) {
postshellcmd += " echo nameserver " + resolvers[i] +
" >> /etc/resolv.conf && ";
}
postshellcmd += "echo " + zone_spec.alias + " > /etc/nodename ; ";
postshellcmd += " sleep 3 ;";
// Setup dhcp on interface if requested
if (zone_spec.net)
zone_spec.net.forEach(function(net) {
if (net.address == "dhcp") {
postshellcmd += " ipadm create-if " + net.physical + ";";
postshellcmd += " ipadm create-addr -T dhcp " + net.physical + "/v4 " +
";";
}
if (zone_spec.brand == "illumos" && net.address != "dhcp") {
postshellcmd += " ipadm create-if " + net.physical + ";";
prefix = ip.IPv4.parse(net.netmask).prefixLengthFromSubnetMask()
postshellcmd += " ipadm create-addr -t -T static -a local=" + net.address + "/" + prefix + " " + net.physical + "/v4 " +
";";
postshellcmd += " route add default " + net.gateway + ";";
}
});
postshellcmd += "sleep 1; ";
postshellcmd +=
" cp /etc/nsswitch.{dns,conf} && svcadm restart routing-setup ";
break;
default:
postshellcmd = null;
}
if (debug.enabled)
console.log("postshellcmd :" + postshellcmd);
return postshellcmd;
}
/*
* Parses --ipaddr="vnic0|192.168.1.1/24|gateway,.."
* to a network json tag
*/
function ipaddrcmd2netobject(ipaddr) {
if (debug.enabled)
console.log(ipaddr);
var networks = ipaddr;
var net = [];
for (var i = 0, l = networks.length; i < l; i++) {
var o = {};
addrnic = networks[i].split("|");
if (addrnic.length < 2) {
console.log(
'not enough parameters in --net=vnic|vm-ip|vm-gateway-ip'
);
return null;
}
o.physical = addrnic[0];
var address = addrnic[1].split("/");
o.address = address[0];
try {
if (o.address != "dhcp") {
if (ip.parse(o.address).kind() == "ipv4") {
o.netmask = ip.IPv4.subnetMaskFromPrefixLength(address[1]).toString();
} else {
o.netmask = ip.IPv6.subnetMaskFromPrefixLength(address[1]).toString();
}
}
} catch (ex) {
console.log("zone definition is invalid", ex.message);
return null;
}
if (o.address != "dhcp")
o.gateway = addrnic[2];
net.push(o);
}
if (debug.enabled)
console.log("network is ", net);
return net;
}
function build(createOptions) {
if (createOptions.debug == true)
debug.enabled = true;
if (debug.enabled)
console.log("CreateOptions ", JSON.stringify(createOptions, null, 4));
if (!isAbletoexec()) {
console.log(
"You must have Primary Administrator Privileges to create zones");
return null;
}
var zname;
if ('docker' in createOptions)
createOptions.brand = 'lx';
if ("net" in createOptions || "docker" in createOptions) {
if ('net' in createOptions) {
if (!vnic_exists(createOptions)) {
return null;
}
createOptions.net =
ipaddrcmd2netobject(createOptions.net);
if (createOptions.net === null) {
return null;
}
}
if ("alias" in createOptions) {
zname = createOptions.alias;
delete createOptions.alias;
} else {
zname = dockerNames.getRandomName(true);
}
createOptions.zonepath = zfs.ZCAGE.VMS + '/' + zname;
createOptions.alias = zname;
if ("ram" in createOptions) {
createOptions.rctl = addrctl(createOptions);
if (createOptions.rctl == null) {
return null;
}
}
if (createOptions['with-image'] && createOptions.disk) {
console.log("--with-image and --disk are mutually exclusive");
return null;
}
if (createOptions.brand === "bhyve" || createOptions.brand === "kvm") {
if (createOptions['with-image'])
createOptions.disk = zfs.GetPool() + '/' + zname;
createOptions.vm_metadata = {
ram: createOptions.ram,
vcpus: createOptions.cpu,
disk: createOptions.disk,
bootdisk: createOptions.disk,
cdrom: createOptions.cdrom,
hostbridge: createOptions.hostbridge,
fs: createOptions.cdrom,
};
if (debug.enabled) {
console.log("vm data",
JSON.stringify(createOptions.vm_metadata, null, 4));
}
delete createOptions.cpu;
delete createOptions.cdrom;
delete createOptions.disk;
delete createOptions.bootdisk;
}
delete createOptions.ram;
delete createOptions.quota;
var zone = getdata(zname);
if (zone != null) {
console.log("Error : alias already exists");
return null;
}
if (!createOptions.docker &&
createOptions['with-image'] &&
(!fs.existsSync(zfs.ZCAGE.IMAGES + '/' + createOptions['with-image'] +
'.zss.gz') &&
!fs.existsSync(zfs.ZCAGE.IMAGES + '/' + createOptions['with-image']))) {
console.log(
"There is no image , first execute: pfexec zcage pull --image <uuid> or zcage fetch <url>."
);
return null;
}
if (createOptions.brand != 'lx' && createOptions.docker) {
console.log("--docker option is only available for lx branded zones");
return null;
}
if ((createOptions.brand == 'lx' || createOptions.brand == 'illumos') && !("with-image" in createOptions) && !
createOptions.docker) {
console.log("--with-image option is required for a lx container");
return null;
}
if (createOptions.udata && (createOptions.brand != 'bhyve' &&
createOptions.brand != 'kvm' || !fs.existsSync(createOptions.udata))) {
console.log("--udata is only valid for a bhyve or kvm brand");
console.log("--udata needs to be a json file with the following format\n" +
' { "userid ": " joe ", " pubkey ": " ssh-rsa key"}');
return null;
}
if (debug.enabled)
console.log("createOptions: " + JSON.stringify(createOptions, null, 4));
var z = create_zone_spec(createOptions);
if (validate_zonespec(z) === null)
return null;
if (create(zname, z) === 0) {
addmeta(zname, false);
if (debug.enabled) {
console.log('Setting container config');
}
if ('docker' in createOptions) {
var tags = createOptions.docker;
zfs.docker_get_config(tags[0], tags[1], zname, addtr_from_docker_img);
}
console.log(chalk `${zname} created [{green.bold OK}] `);
} else {
console.log("Failed creating zone: ", zname);
return -1;
}
if ((createOptions.brand == 'bhyve' || createOptions.brand == 'kvm') &&
z['uses-cloud-init'] == true) {
if (debug.enabled)
console.log("Using cloud-init config" + z.zonepath + '/root/config.iso');
start(zname, z.zonepath