Skip to content

Commit 0fbaf02

Browse files
committed
Merge pull request #40 from ryran/whatness-medimn
implement #38 & #39
2 parents 740e1e0 + 8435246 commit 0fbaf02

File tree

10 files changed

+201
-30
lines changed

10 files changed

+201
-30
lines changed

README.md

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ upvm might not be for you if:
1515
- you already have all the images you need in a RHEV environment
1616
- you want to spin up new machines in an OpenStack cloud
1717

18+
#### I don't like image files! LVM is the best!
19+
I used to use LVM for all my VMs too ... so upvm can use `virt-builder` to generate an image and then write that image out to a block device you specify with the `--primary-blockdev` option. This optional feature of course requires some sudo access (to run [dd-helper.py](https://github.com/ryran/upvm/blob/master/dd-helper.py) as root) which upvm will walk you through the first time you do it.
20+
1821
## Install
1922

2023
#### yum/dnf (RPM) install in RHEL 7.2+ or Fedora 22+:
@@ -51,20 +54,20 @@ Are you annoyed with always having to open the `virt-manager` GUI to do stuff wi
5154
## Help page
5255

5356
```
54-
$ upvm --help
5557
usage: upvm [--loglevel {debug,info,error}] [--build-image-only] [--nocolor]
5658
[--noconsole] [--cachedir VBCACHEDIR] [-h] [--help] [-l]
5759
[--arch {x86_64,i386,i686,ia64,armv71,ppc,ppc64,ppc64le,aarch64,sparc64,s390,s390x}]
5860
[--info] [-n VMNAME] [--os-variant OS] [--root-password SELECTOR]
5961
[--dnsdomain DOMAIN] [--hostname HOSTNAME | --hostname-prompt]
6062
[--no-dhcphostname] [--img-dir DIR] [--img-size SIZE]
6163
[--img-format {auto,raw,qcow2}] [--timezone TIMEZONE]
62-
[--rhsm-key ORGID:KEY] [--ssh-pubkey FILE] [--upload FILE:DEST]
63-
[--run SCRIPT] [--run-command CMD+ARGS] [--firstboot SCRIPT]
64+
[--ssh-pubkey FILE] [--upload FILE:DEST] [--run SCRIPT]
65+
[--run-command CMD+ARGS] [--firstboot SCRIPT]
6466
[--firstboot-command CMD+ARGS] [--install PKG,PKG,@GROUP...]
6567
[--firstboot-install PKG,PKG,@GROUP...] [--selinux-relabel]
66-
[--vbuilder-arg ARG] [-m OPTIONS] [-c OPTIONS] [-d OPTIONS]
67-
[-w OPTIONS] [--boot OPTIONS | --uefi] [--vinstall-arg ARG]
68+
[--vbuilder-arg ARG] [--primary-blockdev BLOCKDEV] [-m OPTIONS]
69+
[-c OPTIONS] [-d OPTIONS] [-w OPTIONS] [--boot OPTIONS | --uefi]
70+
[--vinstall-arg ARG]
6871
[TEMPLATE]
6972
7073
Leverage virt-builder & virt-install to spin up new VMs with ease
@@ -149,17 +152,6 @@ TOTALLY OPTIONAL OS-LEVEL OPTIONS:
149152
syntax, i.e., paths rooted in /usr/share/zoneinfo,
150153
e.g.: 'Europe/London' or 'America/Los_Angeles' or
151154
'Asia/Calcutta')
152-
--rhsm-key ORGID:KEY With or without this option, upvm creates a
153-
/root/register script inside the guest which can be
154-
used to interactively register to RHN Classic or RHSM;
155-
this option edits that script to include an RHSM
156-
activation key created by you at
157-
access.redhat.com/management/activation_keys -- i.e.,
158-
so the script can be run without requiring a
159-
user/password (to use this option, specify both the
160-
organization ID and the activation key, e.g.,
161-
'0123456789:my_activation_key'); IMPORTANT NOTE: this
162-
does not run the /root/register script for you
163155
--ssh-pubkey FILE Inject specific ssh pubkeys (option may be used more
164156
than once) into the /root/.ssh/authorized_keys file of
165157
the guest (default: contents of local
@@ -201,10 +193,18 @@ TOTALLY OPTIONAL OS-LEVEL OPTIONS:
201193
in=/localpath:/mnt/remote')
202194
203195
TOTALLY OPTIONAL HARDWARE-LEVEL (VM) OPTIONS:
204-
Guest hardware configured by virt-install (after image is built). Each option
205-
corresponds with virt-install option of same name. More configurability is
196+
Guest hardware configured by virt-install (after image is built). Most options
197+
correspond with virt-install options of same name. More configurability is
206198
possible than described. See virt-install man page.
207199
200+
--primary-blockdev BLOCKDEV
201+
To use this option, specify a BLOCKDEV like
202+
'/dev/sdb2' or '/dev/vg/logvol' and then in that case,
203+
the image generated by virt-builder [using the above
204+
options] will be written to BLOCKDEV and then deleted;
205+
the first time you try this as a non-root user, upvm
206+
will exit with a warning explaining how to add the
207+
appropriate sudo access to the dd helper-wrapper
208208
-m, --memory OPTIONS If this option is omitted, the guest will be defined
209209
with a RAM allocation of 1024 MiB; in its simplest
210210
form OPTIONS can be the amount of RAM in MiB; more
@@ -262,6 +262,6 @@ ABOUT CONFIG FILES:
262262
override config file values which override defaults.
263263
264264
VERSION:
265-
upvm v0.10.5 last mod 2016/02/14
265+
upvm v0.10.6 last mod 2016/02/16
266266
See <http://github.com/ryran/upvm> to report bugs or RFEs
267267
```

dd-helper.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/python
2+
# -*- coding: utf-8 -*-
3+
#-------------------------------------------------------------------------------
4+
# Copyright 2016 upvm Contributors (see CONTRIBUTORS.md file in source)
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#-------------------------------------------------------------------------------
18+
19+
import subprocess
20+
from sys import argv, exit
21+
from os import devnull
22+
outfile, infile = argv[1], argv[2]
23+
null = open(devnull, 'w')
24+
rc = subprocess.call(['findmnt', '-no', 'target', outfile], stdout=null, stderr=null)
25+
if rc == 0:
26+
# If findmnt reports outfile is mounted
27+
exit(2)
28+
lsof = subprocess.Popen(['nice', 'lsof', '-tlS', outfile], stdout=subprocess.PIPE, stderr=null)
29+
output = lsof.communicate()[0]
30+
if output:
31+
# If lsof reports anything using our outfile
32+
exit(3)
33+
null.close()
34+
dd = ['nice', 'dd', 'of={}'.format(outfile), 'if={}'.format(infile), 'bs=1M']
35+
exit(subprocess.call(dd))

modules/argparser.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ def parse():
190190
grpAA.add_argument(
191191
'--timezone',
192192
help="Set system timezone inside the OS (use traditional syntax, i.e., paths rooted in /usr/share/zoneinfo, e.g.: 'Europe/London' or 'America/Los_Angeles' or 'Asia/Calcutta')")
193-
grpAA.add_argument(
194-
'--rhsm-key', metavar='ORGID:KEY',
195-
help="With or without this option, {} creates a /root/register script inside the guest which can be used to interactively register to RHN Classic or RHSM; this option edits that script to include an RHSM activation key created by you at access.redhat.com/management/activation_keys -- i.e., so the script can be run without requiring a user/password (to use this option, specify both the organization ID and the activation key, e.g., '0123456789:my_activation_key'); IMPORTANT NOTE: this does not run the /root/register script for you".format(cfg.prog))
193+
# grpAA.add_argument(
194+
# '--rhsm-key', metavar='ORGID:KEY',
195+
# help="With or without this option, {} creates a /root/register script inside the guest which can be used to interactively register to RHN Classic or RHSM; this option edits that script to include an RHSM activation key created by you at access.redhat.com/management/activation_keys -- i.e., so the script can be run without requiring a user/password (to use this option, specify both the organization ID and the activation key, e.g., '0123456789:my_activation_key'); IMPORTANT NOTE: this does not run the /root/register script for you".format(cfg.prog))
196196
grpAA.add_argument(
197197
'--ssh-pubkey', metavar='FILE', action='append',
198198
default=['~/.ssh/authorized_keys', '~/.ssh/id_[dr]sa.pub', '~/.ssh/id_ecdsa.pub', '~/.ssh/id_ed25519.pub'],
@@ -227,7 +227,10 @@ def parse():
227227
# Optional virt-install options
228228
grpBB = p.add_argument_group(
229229
'TOTALLY OPTIONAL HARDWARE-LEVEL (VM) OPTIONS',
230-
description="Guest hardware configured by virt-install (after image is built). Each option\ncorresponds with virt-install option of same name. More configurability is\npossible than described. See virt-install man page.")
230+
description="Guest hardware configured by virt-install (after image is built). Most options\ncorrespond with virt-install options of same name. More configurability is\npossible than described. See virt-install man page.")
231+
grpBB.add_argument(
232+
'--primary-blockdev', metavar='BLOCKDEV',
233+
help="To use this option, specify a BLOCKDEV like '/dev/sdb2' or '/dev/vg/logvol' and then in that case, the image generated by virt-builder [using the above options] will be written to BLOCKDEV and then deleted; the first time you try this as a non-root user, {} will exit with a warning explaining how to add the appropriate sudo access to the dd helper-wrapper".format(cfg.prog))
231234
grpBB.add_argument(
232235
'-m', '--memory', metavar='OPTIONS', default=1024,
233236
help="If this option is omitted, the guest will be defined with a RAM allocation of 1024 MiB; in its simplest form OPTIONS can be the amount of RAM in MiB; more complicated example: '-m 512,maxmemory=4096' (which would set the default to 512 MiB but allow raising up to 4 GiB without powering off)")

modules/blockdevimager.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2016 upvm Contributors (see CONTRIBUTORS.md file in source)
3+
# License: Apache License 2.0 (see LICENSE file in source)
4+
5+
# Modules from standard library
6+
from __future__ import print_function
7+
import subprocess
8+
import os
9+
from sys import exit
10+
11+
# Custom modules
12+
from . import string_ops as c
13+
from . import cfg
14+
15+
def write_and_cleanup_image():
16+
blockdev, imgfile = cfg.opts.primary_blockdev, cfg.opts.outFile
17+
cmd = ['sudo', cfg.blockdevHelper, blockdev, imgfile]
18+
print(c.GREEN("Starting write of image file '{}' to blockdev '{}'".format(imgfile, blockdev)))
19+
print(c.green("(This could take a while)"))
20+
rc = subprocess.call(cmd)
21+
if rc == 0:
22+
c.verbose("Successfully wrote intermediate image file to blockdev")
23+
cfg.cleanup_imagefile()
24+
else:
25+
print(c.RED("Error writing image file '{}' to block device '{}'".format(imgfile, blockdev)))
26+
cfg.cleanup_imagefile()
27+
exit(1)

modules/builder.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def build():
7171
initialize_libvirt_qemu_session()
7272
o = cfg.opts
7373
cmd = [
74+
'nice',
7475
'virt-builder', o.templateName,
7576
'--output', o.outFile
7677
]

modules/cfg.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
from . import string_ops as c
1414

1515
# Version info
16-
__version__ = '0.10.5'
17-
__date__ = '2016/02/14'
16+
__version__ = '0.10.6'
17+
__date__ = '2016/02/16'
1818

1919
# All references to program name should use this
2020
prog = 'upvm'
2121

22+
# Path to helper blockdev script
23+
blockdevHelper = '/usr/libexec/{}-dd-helper.py'.format(prog)
24+
2225
# Set config file locations used by configargparse
2326
cfgfileExample = '/usr/share/{}/example.conf'.format(prog)
2427
cfgfileSystem = '/etc/{}.conf'.format(prog)
@@ -74,3 +77,15 @@ def prompt_for_template_and_exit():
7477
exit()
7578
else:
7679
exit(1)
80+
81+
def cleanup_imagefile():
82+
try:
83+
imgfile = opts.outFile
84+
except:
85+
return
86+
try:
87+
os.remove(imgfile)
88+
except:
89+
print(c.yellow("[Cleanup] Deleting image file '{}' failed; you can safely delete it yourself".format(imgfile)))
90+
else:
91+
c.verbose("[Cleanup] Deleted image file '{}'".format(imgfile))

modules/finalprompter.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,10 @@ def check_prompt_hostname():
9696
cfg.opts.hostname = _default_name
9797

9898
def checkset_img_format():
99-
if cfg.opts.img_format in 'auto':
99+
if cfg.opts.primary_blockdev:
100+
c.verbose("Output image format unconditionally set to raw due to --primary-blockdev option")
101+
cfg.opts.img_format = 'raw'
102+
elif cfg.opts.img_format in 'auto':
100103
cfg.opts.img_format = cfg.templateInfo.get('format')
101104
if not cfg.opts.img_format or cfg.opts.img_format == 'qcow2' or cfg.opts.img_format == 'raw':
102105
c.verbose("Unable to determine native format of chosen template")

modules/installer.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from __future__ import print_function
77
import subprocess
88
import os
9+
from sys import exit
910

1011
# Custom modules
1112
from . import cfg
@@ -34,10 +35,12 @@ def install():
3435
str(o.memory),
3536
'--vcpus',
3637
str(o.vcpus),
37-
'--disk',
38-
'{},format={}'.format(o.outFile, o.img_format),
3938
'--noautoconsole',
4039
]
40+
if o.primary_blockdev:
41+
cmd.extend(['--disk', o.primary_blockdev])
42+
else:
43+
cmd.extend(['--disk', '{},format={}'.format(o.outFile, o.img_format)])
4144
if o.os_variant:
4245
cmd.extend(['--os-variant', o.os_variant])
4346
if o.disk:
@@ -58,4 +61,8 @@ def install():
5861
cmd.append(a)
5962
c.verbose("Starting virt-install")
6063
c.debug("Executing:\n {}\n".format(" \ \n ".join(cmd)))
61-
subprocess.check_call(cmd)
64+
rc = subprocess.call(cmd)
65+
if rc != 0:
66+
print(c.RED("Error: virt-install command exited with non-zero status (see above)"))
67+
cfg.cleanup_imagefile()
68+
exit(rc)

modules/sysvalidator.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from sys import exit
1111
import pwd, grp
1212
import json
13+
from stat import S_ISBLK
1314

1415
# Custom modules
1516
from . import cfg
@@ -38,6 +39,27 @@ def call(cmd, showStdout=False, showStderr=False, shell=False):
3839
null.close()
3940
return ret(rc)
4041

42+
def convert_size_string_to_bytes(size):
43+
unit = size[-1:]
44+
sz = float(size[:-1])
45+
if unit == 'b':
46+
return sz
47+
elif unit == 'K':
48+
return sz * 1024**1
49+
elif unit == 'M':
50+
return sz * 1024**2
51+
elif unit == 'G':
52+
return sz * 1024**3
53+
elif unit == 'T':
54+
return sz * 1024**4
55+
elif unit == 'P':
56+
return sz * 1024**5
57+
elif unit == 'E':
58+
return sz * 1024**6
59+
else:
60+
print(c.RED("Invalid size specified (use e.g., '256M', '10G')"))
61+
exit(1)
62+
4163
def print_template_metadata(template):
4264
print("{0:17}: {1}".format("template", template['os-version']))
4365
for key in template:
@@ -151,8 +173,57 @@ def try_capture_existing_vm_names():
151173
print(c.RED("\nUnknown error executing '{}'".format(" ".join(cmd))))
152174
exit(1)
153175

176+
def validate_image_size():
177+
if cfg.opts.img_size:
178+
return convert_size_string_to_bytes(cfg.opts.img_size)
179+
else:
180+
return float(cfg.templateInfo['size'])
181+
182+
def validate_blockdev(imgsize):
183+
blockdev = cfg.opts.primary_blockdev
184+
try:
185+
mode = os.stat(blockdev).st_mode
186+
except OSError, e:
187+
print(c.RED(e))
188+
print("It looks like you passed the wrong thing to the --primary-blockdev option")
189+
exit(1)
190+
if not S_ISBLK(mode):
191+
print(c.RED("File passed as an arg to the --primary-blockdev option is not a block device"))
192+
print("This option is meant to be used to create a VM whose primary storage is a block device like /dev/sdb2")
193+
exit(1)
194+
cmd = ['lsblk', '-no', 'size', blockdev]
195+
c.debug("Executing: {}".format(" ".join(cmd)))
196+
try:
197+
blksize = subprocess.check_output(cmd).strip()
198+
except:
199+
print(c.RED("Unexpected error using lsblk to check size of --primary-blockdev device"))
200+
print("This shouldn't happen ... unless you somehow don't have util-linux and the lsblk command")
201+
exit(1)
202+
blksize = convert_size_string_to_bytes(blksize)
203+
if imgsize > blksize:
204+
print(c.YELLOW("Aborting because block device '{}' is smaller than projected image size".format(blockdev)))
205+
print("Block device size: {} MiB".format(int(blksize/1024/1024)))
206+
print("Image file size: {} MiB".format(int(imgsize/1024/1024)))
207+
exit(1)
208+
209+
def check_sudo_blockdevHelper_rights():
210+
blockdev, imgdir = cfg.opts.primary_blockdev, cfg.opts.img_dir
211+
cmd = ['sudo', '-l', cfg.blockdevHelper, blockdev, imgdir]
212+
if not call(cmd):
213+
print(c.YELLOW("Need root privileges to write image to {}".format(blockdev)))
214+
print(c.yellow(
215+
"You've requested {} write a VM disk image out to a block device but you\n"
216+
"are not root. To do this, give yourself the appropriate sudo access by\n"
217+
"running the following command as root:".format(cfg.prog)))
218+
print(c.green(" echo '{} ALL=(ALL) NOPASSWD: {}' >>/etc/sudoers.d/{}\n".format(myUser, cfg.blockdevHelper, cfg.prog)))
219+
print(c.yellow(
220+
"If you want to restrict your user to only have write access to a specific\n"
221+
"LVM volume group, you could instead do something like:"))
222+
print(c.green(" echo '{} ALL=(ALL) NOPASSWD: {} /dev/volgroup*' >>/etc/sudoers.d/{}\n".format(myUser, cfg.blockdevHelper, cfg.prog)))
223+
exit(1)
224+
154225
def check_system_config():
155-
check_virtbuilder_version()
226+
##check_virtbuilder_version()
156227
if_no_template_requested_then_print_vblist_and_quit()
157228
check_if_requested_template_exists()
158229
isolate_metadata_for_chosen_vb_template()
@@ -164,3 +235,7 @@ def check_system_config():
164235
##check_user_in_libvirt_group()
165236
check_for_writable_imgdir()
166237
try_capture_existing_vm_names()
238+
imgsize = validate_image_size()
239+
if cfg.opts.primary_blockdev:
240+
validate_blockdev(imgsize)
241+
check_sudo_blockdevHelper_rights()

upvm.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ def main():
8787
# Quit if requested
8888
if cfg.opts.build_image_only:
8989
exit()
90+
# Write image to blockdevice if requested
91+
if cfg.opts.primary_blockdev:
92+
from modules import blockdevimager
93+
blockdevimager.write_and_cleanup_image()
9094
# Launch virt-install
9195
from modules import installer
9296
installer.install()
@@ -101,4 +105,5 @@ def main():
101105
main()
102106
except KeyboardInterrupt:
103107
print("\nReceived KeyboardInterrupt. Exiting.")
108+
cfg.cleanup_imagefile()
104109
exit()

0 commit comments

Comments
 (0)