107
108
*args, from_unicode=path_from_unicode, **kwargs)
111
if bzrlib.version_info < (2, 6):
112
class RegistryOption(config.Option):
113
"""Option for a choice from a registry."""
115
def __init__(self, name, registry, default_from_env=None,
116
help=None, invalid=None):
117
"""A registry based Option definition.
119
This overrides the base class so the conversion from a unicode
120
string can take quoting into account.
122
super(RegistryOption, self).__init__(
123
name, default=lambda: unicode(registry.default_key),
124
default_from_env=default_from_env,
125
from_unicode=self.from_unicode, help=help,
126
invalid=invalid, unquote=False)
127
self.registry = registry
129
def from_unicode(self, unicode_str):
130
if not isinstance(unicode_str, basestring):
133
return self.registry.get(unicode_str)
136
"Invalid value %s for %s."
137
"See help for a list of possible values."
138
% (unicode_str, self.name))
141
RegistryOption = config.RegistryOption
144
# The VM classes are registered later (where they are defined)
145
vm_class_registry = registry.Registry()
110
148
def register(option):
111
149
config.option_registry.register(option)
739
786
return '#cloud-config-archive\n' + yaml.safe_dump(parts)
742
def vm_states(source=None):
743
"""A dict of states for vms indexed by name.
745
:param source: A list of lines as produced by virsh list --all without
746
decorations (header/footer).
749
retcode, out, err = run_subprocess(['virsh', 'list', '--all'])
750
# Get rid of header/footer
751
source = out.splitlines()[2:-1]
754
caret_or_id, name, state = line.split(None, 2)
759
789
class VM(object):
790
"""A virtual machine relying on cloud-init to customize installation."""
761
792
def __init__(self, conf):
763
794
self._config_dir = None
796
self._meta_data_path = None
797
self._user_data_path = None
799
def _download_in_cache(self, source_url, name, force=False):
800
"""Download ``name`` from ``source_url`` in ``vm.download_cache``.
802
:param source_url: The url where the file to download is located
804
:param name: The name of the file to download (also used as the name
805
for the downloaded file).
807
:param force: Remove the file from the cache if present.
809
:return: False if the file is in the download cache, True if a download
812
source = urlutils.join(source_url, name)
813
download_dir = self.conf.get('vm.download_cache')
814
if not os.path.exists(download_dir):
815
raise ConfigValueError('vm.download_cache', download_dir)
816
target = os.path.join(download_dir, name)
817
# FIXME: By default the download dir may be under root control, but if
818
# a user chose to use a different one under his own control, it would
819
# be nice to not require sudo usage. -- vila 2013-02-06
821
run_subprocess(['sudo', 'rm', '-f', target])
822
if not os.path.exists(target):
823
# FIXME: We do ask for a progress bar but it's not displayed
824
# (run_subprocess capture both stdout and stderr) ! At least while
825
# used interactively, it should. -- vila 2013-02-06
826
run_subprocess(['sudo', 'wget', '--progress=dot:mega', '-O',
765
832
def ensure_dir(self, path):
794
861
self._ssh_keygen(key)
863
def create_meta_data(self):
864
self.ensure_config_dir()
865
self._meta_data_path = os.path.join(self._config_dir, 'meta-data')
866
with open(self._meta_data_path, 'w') as f:
867
f.write(self.conf.get('vm.meta_data'))
869
def create_user_data(self):
870
ci_user_data = CIUserData(self.conf)
871
ci_user_data.populate()
872
self.ensure_config_dir()
873
self._user_data_path = os.path.join(self._config_dir, 'user-data')
874
with open(self._user_data_path, 'w') as f:
875
f.write(ci_user_data.dump())
877
def download(self, force=False):
878
raise NotImplementedError(self.download)
880
def parse_console_during_install(self, cmd):
881
"""Parse the console output until the end of the install.
883
We added a specific part for cloud-init to ensure we properly detect
886
:param cmd: The install command (used for error display).
888
console = FileMonitor(self._console_path)
890
for line in console.parse():
891
# FIXME: We need some way to activate this dynamically (conf var defaulting to
892
# env var OR cmdline parameter ? -- vila 2013-02-11
893
# print "read: [%s]" % (line,) # so useful for debug...
895
except (ConsoleEOFError, CloudInitError):
896
# FIXME: No test covers this path -- vila 2013-02-15
897
err_lines = ['Suspicious line from cloud-init.\n',
898
'\t' + console.lines[-1],
899
'Check the configuration:\n']
900
with open(self._meta_data_path) as f:
901
err_lines.append('meta-data content:\n')
902
err_lines.extend(f.readlines())
903
with open(self._user_data_path) as f:
904
err_lines.append('user-data content:\n')
905
err_lines.extend(f.readlines())
906
raise CommandError(cmd, console.proc.returncode,
907
'\n'.join(console.lines),
911
def kvm_states(source=None):
912
"""A dict of states for kvms indexed by name.
914
:param source: A list of lines as produced by virsh list --all without
915
decorations (header/footer).
918
retcode, out, err = run_subprocess(['virsh', 'list', '--all'])
919
# Get rid of header/footer
920
source = out.splitlines()[2:-1]
923
caret_or_id, name, state = line.split(None, 2)
799
930
def __init__(self, conf):
800
931
super(Kvm, self).__init__(conf)
802
self._meta_data_path = None
803
self._user_data_path = None
933
self._disk_image_path = None
805
934
self._seed_path = None
806
self._disk_image_path = None
808
936
self._console_path = None
810
def _download_in_cache(self, source_url, name, force=False):
811
"""Download ``name`` from ``source_url`` in ``vm.download_cache``.
813
:param source_url: The url where the file to download is located
815
:param name: The name of the file to download (also used as the name
816
for the downloaded file).
818
:param force: Remove the file from the cache if present.
820
:return: False if the file is in the download cache, True if a download
823
source = urlutils.join(source_url, name)
824
download_dir = self.conf.get('vm.download_cache')
825
if not os.path.exists(download_dir):
826
raise ConfigValueError('vm.download_cache', download_dir)
827
target = os.path.join(download_dir, name)
828
# FIXME: By default the download dir may be under root control, but if
829
# a user chose to use a different one under his own control, it would
830
# be nice to not require sudo usage. -- vila 2013-02-06
832
run_subprocess(['sudo', 'rm', '-f', target])
833
if not os.path.exists(target):
834
# FIXME: We do ask for a progress bar but it's not displayed
835
# (run_subprocess capture both stdout and stderr) ! At least while
836
# used interactively, it should. -- vila 2013-02-06
837
run_subprocess(['sudo', 'wget', '--progress=dot:mega', '-O',
939
states = kvm_states()
941
state = states[self.conf.get('vm.name')]
843
946
def download_iso(self, force=False):
844
947
"""Download the iso to install the vm.
895
989
self._seed_path = seed_path
897
991
def create_disk_image(self):
898
raise NotImplementedError(self.create_disk_image)
900
def _wait_for_install_with_seed(self):
992
if self.conf.get('vm.backing') is None:
993
self.create_disk_image_from_cloud_image()
995
self.create_disk_image_from_backing()
997
def create_disk_image_from_cloud_image(self):
998
"""Create a disk image from a cloud one."""
999
cloud_image_path = os.path.join(
1000
self.conf.get('vm.download_cache'),
1001
self.conf.get('vm.cloud_image_name'))
1002
disk_image_path = os.path.join(
1003
self.conf.get('vm.images_dir'),
1004
self.conf.expand_options('{vm.name}.qcow2'))
1006
['sudo', 'qemu-img', 'convert',
1007
'-O', 'qcow2', cloud_image_path, disk_image_path])
1009
['sudo', 'qemu-img', 'resize',
1010
disk_image_path, self.conf.get('vm.disk_size')])
1011
self._disk_image_path = disk_image_path
1013
def create_disk_image_from_backing(self):
1014
"""Create a disk image backed by an existing one."""
1015
backing_image_path = os.path.join(
1016
self.conf.get('vm.images_dir'),
1017
self.conf.expand_options('{vm.backing}'))
1018
disk_image_path = os.path.join(
1019
self.conf.get('vm.images_dir'),
1020
self.conf.expand_options('{vm.name}.qcow2'))
1022
['sudo', 'qemu-img', 'create', '-f', 'qcow2',
1023
'-b', backing_image_path, disk_image_path])
1025
['sudo', 'qemu-img', 'resize',
1026
disk_image_path, self.conf.get('vm.disk_size')])
1027
self._disk_image_path = disk_image_path
1029
def parse_console_during_install(self, cmd):
1030
"""See Vm.parse_console_during_install."""
901
1031
# The console is created by virt-install which requires sudo but
902
1032
# creates the file 0600 for libvirt-qemu. We give read access to all
903
1033
# otherwise 'tail -f' requires sudo and can't be killed anymore.
904
1034
run_subprocess(['sudo', 'chmod', '0644', self._console_path])
905
1035
# While `virt-install` is running, let's connect to the console
906
console = FileMonitor(self._console_path)
908
for line in console.parse():
909
# FIXME: We need some way to activate this dynamically (conf var defaulting to
910
# env var OR cmdline parameter ? -- vila 2013-02-11
911
# print "read: [%s]" % (line,) # so useful for debug...
913
except (ConsoleEOFError, CloudInitError):
914
# FIXME: No test covers this path -- vila 2013-02-15
915
err_lines = ['Suspicious line from cloud-init.\n',
916
'\t' + console.lines[-1],
917
'Check the configuration:\n']
918
with open(self._meta_data_path) as f:
919
err_lines.append('meta-data content:\n')
920
err_lines.extend(f.readlines())
921
with open(self._user_data_path) as f:
922
err_lines.append('user-data content:\n')
923
err_lines.extend(f.readlines())
924
raise CommandError(console.cmd, console.proc.returncode,
925
'\n'.join(console.lines),
1036
super(Kvm, self).parse_console_during_install(cmd)
928
1038
def install(self):
929
1039
# Create a kvm, relying on cloud-init to customize the base image.
952
1062
self._console_path = os.path.join(
953
1063
self.conf.get('vm.images_dir'),
954
1064
'%s.console' % (self.conf.get('vm.name'),))
956
['sudo', 'virt-install',
957
# To ensure we're not bitten again by http://pad.lv/1157272 where
958
# virt-install wrongly detect virtualbox. -- vila 2013-03-20
959
'--connect', 'qemu:///system',
960
# Without --noautoconsole, virt-install will relay the console,
961
# that's not appropriate for our needs so we'll connect later
964
# We define the console as a file so we can monitor the install
966
'--serial', 'file,path=%s' % (self._console_path,),
967
'--network', self.conf.get('vm.network'),
968
# Anticipate that we'll need a graphic card defined
969
'--graphics', 'spice',
970
'--name', self.conf.get('vm.name'),
971
'--ram', self.conf.get('vm.ram_size'),
972
'--vcpus', self.conf.get('vm.cpus'),
973
'--disk', 'path=%s,format=qcow2' % (self._disk_image_path,),
974
'--disk', 'path=%s' % (self._seed_path,),
975
# We just boot, cloud-init will handle the installs we need
976
'--boot', 'hd', '--hvm',
978
self._wait_for_install_with_seed()
1066
'sudo', 'virt-install',
1067
# To ensure we're not bitten again by http://pad.lv/1157272 where
1068
# virt-install wrongly detect virtualbox. -- vila 2013-03-20
1069
'--connect', 'qemu:///system',
1070
# Without --noautoconsole, virt-install will relay the console,
1071
# that's not appropriate for our needs so we'll connect later
1074
# We define the console as a file so we can monitor the install
1076
'--serial', 'file,path=%s' % (self._console_path,),
1077
'--network', self.conf.get('vm.network'),
1078
# Anticipate that we'll need a graphic card defined
1079
'--graphics', 'spice',
1080
'--name', self.conf.get('vm.name'),
1081
'--ram', self.conf.get('vm.ram_size'),
1082
'--vcpus', self.conf.get('vm.cpus'),
1083
'--disk', 'path=%s,format=qcow2' % (self._disk_image_path,),
1084
'--disk', 'path=%s' % (self._seed_path,),
1085
# We just boot, cloud-init will handle the installs we need
1086
'--boot', 'hd', '--hvm',
1088
run_subprocess(virt_install)
1089
self.parse_console_during_install(virt_install)
979
1090
# We've seen the console signaling halt, but the vm will need a bit
980
1091
# more time to get there so we help it a bit.
981
1092
if self.conf.get('vm.release') in ('precise', 'quantal'):
1016
1127
'--remove-all-storage'])
1019
class KvmFromCloudImage(Kvm):
1021
def create_disk_image(self, src_name=None, dst_name=None):
1022
"""Create a disk image from a cloud one."""
1023
if src_name is None:
1024
src_name = self.conf.get('vm.cloud_image_name')
1025
if dst_name is None:
1026
dst_name = self.conf.expand_options('{vm.name}.qcow2')
1027
cloud_image_path = os.path.join(
1028
self.conf.get('vm.download_cache'), src_name)
1029
disk_image_path = os.path.join(
1030
self.conf.get('vm.images_dir'), dst_name)
1032
['sudo', 'qemu-img', 'convert',
1033
'-O', 'qcow2', cloud_image_path, disk_image_path])
1035
['sudo', 'qemu-img', 'resize',
1036
disk_image_path, self.conf.get('vm.disk_size')])
1037
self._disk_image_path = disk_image_path
1040
class KvmFromBacking(Kvm):
1042
def create_disk_image(self, src_name=None, dst_name=None):
1043
"""Create a disk image backed by an existing one."""
1044
backing_image_path = os.path.join(
1045
self.conf.get('vm.images_dir'),
1046
self.conf.expand_options('{vm.backing}'))
1047
disk_image_path = os.path.join(
1048
self.conf.get('vm.images_dir'),
1049
self.conf.expand_options('{vm.name}.qcow2'))
1051
['sudo', 'qemu-img', 'create', '-f', 'qcow2',
1052
'-b', backing_image_path, disk_image_path])
1054
['sudo', 'qemu-img', 'resize',
1055
disk_image_path, self.conf.get('vm.disk_size')])
1056
self._disk_image_path = disk_image_path
1130
vm_class_registry.register('kvm', Kvm, 'Kernel-based virtual machine')
1133
def lxc_info(vm_name, source=None):
1134
"""Parse state info from the lxc-info output.
1136
:param vm_name: The vm we want to query about.
1138
:param source: A list of lines as produced by virsh list --all without
1139
decorations (header/footer).
1142
retcode, out, err = run_subprocess(['sudo', 'lxc-info', '-n', vm_name])
1143
source = out.splitlines()
1144
state_line, pid_line = source
1145
_, state = state_line.split(None, 1)
1146
_, pid = pid_line.split(None, 1)
1147
return dict(state=state, pid=pid)
1152
def __init__(self, conf):
1153
super(Lxc, self).__init__(conf)
1154
self._guest_seed_path = None
1155
self._fstab_path = None
1158
info = lxc_info(self.conf.get('vm.name'))
1159
return info['state']
1161
def download(self, force=False):
1162
# FIXME: lxc-create provides its own cache. download(True) should just
1163
# ensure we clear that cache from the previous download. Should we add
1164
# a warning ? Specialize the cache for Kvm only ?-- vila 2013-08-07
1167
def create_seed_files(self):
1168
if self._meta_data_path is None:
1169
self.create_meta_data()
1170
if self._user_data_path is None:
1171
self.create_user_data()
1172
self._fstab_path = os.path.join(self._config_dir, 'fstab')
1173
self._guest_seed_path = os.path.join(
1174
self.conf.get('vm.lxcs_dir'),
1175
self.conf.get('vm.name'),
1176
'rootfs/var/lib/cloud/seed/nocloud-net')
1177
with open(self._fstab_path, 'w') as f:
1178
# Add a entry so cloud-init find the seed files
1179
f.write('%s %s none bind 0 0\n' % (self._config_dir,
1180
self._guest_seed_path))
1183
'''Create an lxc, relying on cloud-init to customize the base image.
1185
There are two processes involvded here:
1186
- lxc-create creates the vm.
1187
- progress is monitored via the console to detect cloud-final.
1189
Once cloud-init has finished, the vm can be powered off.
1191
# FIXME: If the install doesn't finish after $time, emit a warning and
1192
# terminate self.install_proc.
1193
# FIXME: If we can't connect to the console, emit a warning and
1194
# terminate console and self.install_proc.
1195
# FIXME: If we don't receive anything on the console after $time2, emit
1196
# a warning and terminate console and self.install_proc.
1197
# -- vila 2013-02-07
1198
if self._fstab_path is None:
1199
self.create_seed_files()
1200
# FIXME: Install time is probably a good time to delete the
1201
# console. While it makes sense to accumulate for all runs for a given
1202
# install, keeping them without any limit nor roration is likely to
1203
# cause issues at some point... -- vila 2013-02-20
1204
self._console_path = os.path.join(
1205
# FIXME: We use _config_dir instead of 'vm.images_dir' as kvm does
1206
# because the later is owned by root so we can't create a file
1207
# there. It would be nice to check if the same trick can be used
1208
# for kvm to simplify. -- vila 2013-08-07
1210
'%s.console' % (self.conf.get('vm.name'),))
1211
# Create/empty the file so we get access to it (otherwise it will be
1213
open(self._console_path, 'w').close()
1214
# FIXME: Some feedback would be nice during lxc creation, not sure
1215
# about which errors to expect there either -- vila 2013-08-07
1217
['sudo', 'lxc-create',
1218
'-n', self.conf.get('vm.name'),
1219
'-t', 'ubuntu-cloud',
1221
'-r', self.conf.get('vm.release'),
1222
'-a', self.conf.get('vm.cpu_model'),
1223
'-C', # From cloud image, implying download/cache
1225
# Now we add the cloud-init data seed and do lxc-start to trigger all
1226
# our customizations monitoring the lxc-start output from the host.
1227
mkdir_seed_path = 'mkdir -p %s' % (self._guest_seed_path,)
1228
lxc_start = ['sudo', 'lxc-start',
1229
'-n', self.conf.get('vm.name'),
1230
'--define', 'lxc.hook.pre-start=%s' % (mkdir_seed_path,),
1231
'--define', 'lxc.mount=%s' % (self._fstab_path,),
1232
'--console-log', self._console_path,
1233
# Daemonize or: 1) it fails with a spurious return code,
1234
# 2) We can't monitor the logfile
1237
run_subprocess(lxc_start)
1238
self.parse_console_during_install(lxc_start)
1241
return run_subprocess(
1242
['sudo', 'lxc-stop', '-n', self.conf.get('vm.name')])
1246
return run_subprocess(
1247
['sudo', 'lxc-destroy', '-n', self.conf.get('vm.name')])
1248
except CommandError as e:
1249
# FIXME: No test -- vila 2013-08-08
1250
if e.err.endswith('does not exist\n'):
1251
# Fine. lxc-info makes no distinction between a stopped vm and
1252
# a non-existing one.
1258
vm_class_registry.register('lxc', Lxc, 'Linux container virtual machine')
1059
1261
class ArgParser(argparse.ArgumentParser):