~canonical-isd-hackers/u1-test-utils/trunk

« back to all changes in this revision

Viewing changes to setup_vm/bin/setup_vm.py

  • Committer: Tarmac
  • Author(s): Vincent Ladeuil
  • Date: 2013-08-09 09:43:05 UTC
  • mfrom: (90.2.25 lxc)
  • Revision ID: tarmac-20130809094305-tv8wqjbpdgw9fwrm
[r=elopio] Start implementing lxc support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
17
17
import bzrlib
18
18
from bzrlib import (
19
19
    config,
 
20
    registry,
20
21
    transport,
21
22
    urlutils,
22
23
    )
107
108
            *args, from_unicode=path_from_unicode, **kwargs)
108
109
 
109
110
 
 
111
if bzrlib.version_info < (2, 6):
 
112
    class RegistryOption(config.Option):
 
113
        """Option for a choice from a registry."""
 
114
 
 
115
        def __init__(self, name, registry, default_from_env=None,
 
116
                     help=None, invalid=None):
 
117
            """A registry based Option definition.
 
118
 
 
119
            This overrides the base class so the conversion from a unicode
 
120
            string can take quoting into account.
 
121
            """
 
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
 
128
 
 
129
        def from_unicode(self, unicode_str):
 
130
            if not isinstance(unicode_str, basestring):
 
131
                raise TypeError
 
132
            try:
 
133
                return self.registry.get(unicode_str)
 
134
            except KeyError:
 
135
                raise ValueError(
 
136
                    "Invalid value %s for %s."
 
137
                    "See help for a list of possible values."
 
138
                    % (unicode_str, self.name))
 
139
 
 
140
else:
 
141
    RegistryOption = config.RegistryOption
 
142
 
 
143
 
 
144
# The VM classes are registered later (where they are defined)
 
145
vm_class_registry = registry.Registry()
 
146
 
 
147
 
110
148
def register(option):
111
149
    config.option_registry.register(option)
112
150
 
160
198
                       help='''\
161
199
Where libvirt (qemu) stores the vms config files.'''))
162
200
 
 
201
# The base directories where vms are stored for lxc
 
202
register(PathOption('vm.lxcs_dir', default='/var/lib/lxc',
 
203
                    help='''Where lxc definitions are stored.'''))
163
204
# Isos and images download handling
164
205
register(config.Option('vm.iso_url',
165
206
                       default='http://cdimage.ubuntu.com/daily-live/current/',
178
219
register(PathOption('vm.download_cache', default='{vm.images_dir}',
179
220
                    help='''Where downloads end up.'''))
180
221
 
 
222
 
 
223
register(RegistryOption('vm.class', vm_class_registry,
 
224
                        invalid='error',
 
225
                        help='''The virtual machine technology to use.'''))
181
226
# The ubiquitous vm name
182
227
register(config.Option('vm.name', default=None, invalid='error',
183
228
                       help='''\
633
678
        # can't create any dir/file there. The fix is to only create a script
634
679
        # that will be executed via runcmd so it will run later and avoid the
635
680
        # issue. -- vila 2013-03-21
 
681
        # FIXME: Moreover, -pristine vms don't have bzr installed so this
 
682
        # cannot succeed there -- vila 2013-08-07
636
683
        hook_content = '''#!/bin/sh
637
684
mkdir -p {dir_path}
638
685
chown {user}:{user} ~ubuntu
739
786
        return '#cloud-config-archive\n' + yaml.safe_dump(parts)
740
787
 
741
788
 
742
 
def vm_states(source=None):
743
 
    """A dict of states for vms indexed by name.
744
 
 
745
 
    :param source: A list of lines as produced by virsh list --all without
746
 
        decorations (header/footer).
747
 
    """
748
 
    if source is None:
749
 
        retcode, out, err = run_subprocess(['virsh', 'list', '--all'])
750
 
        # Get rid of header/footer
751
 
        source = out.splitlines()[2:-1]
752
 
    states = {}
753
 
    for line in source:
754
 
        caret_or_id, name, state = line.split(None, 2)
755
 
        states[name] = state
756
 
    return states
757
 
 
758
 
 
759
789
class VM(object):
 
790
    """A virtual machine relying on cloud-init to customize installation."""
760
791
 
761
792
    def __init__(self, conf):
762
793
        self.conf = conf
763
794
        self._config_dir = None
 
795
        # Seed files
 
796
        self._meta_data_path = None
 
797
        self._user_data_path = None
 
798
 
 
799
    def _download_in_cache(self, source_url, name, force=False):
 
800
        """Download ``name`` from ``source_url`` in ``vm.download_cache``.
 
801
 
 
802
        :param source_url: The url where the file to download is located
 
803
 
 
804
        :param name: The name of the file to download (also used as the name
 
805
            for the downloaded file).
 
806
 
 
807
        :param force: Remove the file from the cache if present.
 
808
 
 
809
        :return: False if the file is in the download cache, True if a download
 
810
            occurred.
 
811
        """
 
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
 
820
        if force:
 
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',
 
827
                            target, source])
 
828
            return True
 
829
        else:
 
830
            return False
764
831
 
765
832
    def ensure_dir(self, path):
766
833
        try:
793
860
        for key in keys:
794
861
            self._ssh_keygen(key)
795
862
 
 
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'))
 
868
 
 
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())
 
876
 
 
877
    def download(self, force=False):
 
878
        raise NotImplementedError(self.download)
 
879
 
 
880
    def parse_console_during_install(self, cmd):
 
881
        """Parse the console output until the end of the install.
 
882
 
 
883
        We added a specific part for cloud-init to ensure we properly detect
 
884
        the end of the run.
 
885
 
 
886
        :param cmd: The install command (used for error display).
 
887
        """
 
888
        console = FileMonitor(self._console_path)
 
889
        try:
 
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...
 
894
                pass
 
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),
 
908
                               ''.join(err_lines))
 
909
 
 
910
 
 
911
def kvm_states(source=None):
 
912
    """A dict of states for kvms indexed by name.
 
913
 
 
914
    :param source: A list of lines as produced by virsh list --all without
 
915
        decorations (header/footer).
 
916
    """
 
917
    if source is None:
 
918
        retcode, out, err = run_subprocess(['virsh', 'list', '--all'])
 
919
        # Get rid of header/footer
 
920
        source = out.splitlines()[2:-1]
 
921
    states = {}
 
922
    for line in source:
 
923
        caret_or_id, name, state = line.split(None, 2)
 
924
        states[name] = state
 
925
    return states
 
926
 
796
927
 
797
928
class Kvm(VM):
798
929
 
799
930
    def __init__(self, conf):
800
931
        super(Kvm, self).__init__(conf)
801
 
        # Seed files
802
 
        self._meta_data_path = None
803
 
        self._user_data_path = None
804
932
        # Disk paths
 
933
        self._disk_image_path = None
805
934
        self._seed_path = None
806
 
        self._disk_image_path = None
807
935
 
808
936
        self._console_path = None
809
937
 
810
 
    def _download_in_cache(self, source_url, name, force=False):
811
 
        """Download ``name`` from ``source_url`` in ``vm.download_cache``.
812
 
 
813
 
        :param source_url: The url where the file to download is located
814
 
 
815
 
        :param name: The name of the file to download (also used as the name
816
 
            for the downloaded file).
817
 
 
818
 
        :param force: Remove the file from the cache if present.
819
 
 
820
 
        :return: False if the file is in the download cache, True if a download
821
 
            occurred.
822
 
        """
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
831
 
        if force:
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',
838
 
                            target, source])
839
 
            return True
840
 
        else:
841
 
            return False
 
938
    def state(self):
 
939
        states = kvm_states()
 
940
        try:
 
941
            state = states[self.conf.get('vm.name')]
 
942
        except KeyError:
 
943
            state = None
 
944
        return state
842
945
 
843
946
    def download_iso(self, force=False):
844
947
        """Download the iso to install the vm.
860
963
                                       self.conf.get('vm.cloud_image_name'),
861
964
                                       force=force)
862
965
 
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'))
868
 
 
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())
876
 
 
877
 
    def create_seed(self):
 
966
    def download(self, force=False):
 
967
        return self.download_cloud_image(force)
 
968
 
 
969
    def create_seed_image(self):
878
970
        if self._meta_data_path is None:
879
971
            self.create_meta_data()
880
972
        if self._user_data_path is None:
886
978
            # We create the seed in the ``vm.images_dir`` directory, so
887
979
            # ``sudo`` is required
888
980
            ['sudo',
889
 
             'genisoimage', '-output', seed_path, '-volid', 'cidata',
 
981
             'genisoimage', '-output', seed_path,
 
982
             # cloud-init relies on the volid to discover its data
 
983
             '-volid', 'cidata',
890
984
             '-joliet', '-rock', '-input-charset', 'default',
891
985
             '-graft-points',
892
986
             'user-data=%s' % (self._user_data_path,),
895
989
        self._seed_path = seed_path
896
990
 
897
991
    def create_disk_image(self):
898
 
        raise NotImplementedError(self.create_disk_image)
899
 
 
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()
 
994
        else:
 
995
            self.create_disk_image_from_backing()
 
996
 
 
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'))
 
1005
        run_subprocess(
 
1006
            ['sudo', 'qemu-img', 'convert',
 
1007
             '-O', 'qcow2', cloud_image_path, disk_image_path])
 
1008
        run_subprocess(
 
1009
            ['sudo', 'qemu-img', 'resize',
 
1010
             disk_image_path, self.conf.get('vm.disk_size')])
 
1011
        self._disk_image_path = disk_image_path
 
1012
 
 
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'))
 
1021
        run_subprocess(
 
1022
            ['sudo', 'qemu-img', 'create', '-f', 'qcow2',
 
1023
             '-b', backing_image_path, disk_image_path])
 
1024
        run_subprocess(
 
1025
            ['sudo', 'qemu-img', 'resize',
 
1026
             disk_image_path, self.conf.get('vm.disk_size')])
 
1027
        self._disk_image_path = disk_image_path
 
1028
 
 
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)
907
 
        try:
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...
912
 
                pass
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),
926
 
                               ''.join(err_lines))
 
1036
        super(Kvm, self).parse_console_during_install(cmd)
927
1037
 
928
1038
    def install(self):
929
1039
        # Create a kvm, relying on cloud-init to customize the base image.
942
1052
        # a warning and terminate console and self.install_proc.
943
1053
        # -- vila 2013-02-07
944
1054
        if self._seed_path is None:
945
 
            self.create_seed()
 
1055
            self.create_seed_image()
946
1056
        if self._disk_image_path is None:
947
1057
            self.create_disk_image()
948
1058
        # FIXME: Install time is probably a good time to delete the
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'),))
955
 
        run_subprocess(
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
962
 
             # ourselves
963
 
             '--noautoconsole',
964
 
             # We define the console as a file so we can monitor the install
965
 
             # via 'tail -f'
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',
977
 
             ])
978
 
        self._wait_for_install_with_seed()
 
1065
        virt_install = [
 
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
 
1072
            # ourselves
 
1073
            '--noautoconsole',
 
1074
            # We define the console as a file so we can monitor the install
 
1075
            # via 'tail -f'
 
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',
 
1087
            ]
 
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'):
984
1095
            self.poweroff()
985
1096
        vm_name = self.conf.get('vm.name')
986
1097
        while True:
987
 
            state = vm_states()[vm_name]
 
1098
            state = self.state()
988
1099
            # We expect the vm's state to be 'in shutdown' but in some rare
989
1100
            # occasions we may catch 'running' before getting 'in shutdown'.
990
1101
            if state in ('in shutdown', 'running'):
1016
1127
             '--remove-all-storage'])
1017
1128
 
1018
1129
 
1019
 
class KvmFromCloudImage(Kvm):
1020
 
 
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)
1031
 
        run_subprocess(
1032
 
            ['sudo', 'qemu-img', 'convert',
1033
 
             '-O', 'qcow2', cloud_image_path, disk_image_path])
1034
 
        run_subprocess(
1035
 
            ['sudo', 'qemu-img', 'resize',
1036
 
             disk_image_path, self.conf.get('vm.disk_size')])
1037
 
        self._disk_image_path = disk_image_path
1038
 
 
1039
 
 
1040
 
class KvmFromBacking(Kvm):
1041
 
 
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'))
1050
 
        run_subprocess(
1051
 
            ['sudo', 'qemu-img', 'create', '-f', 'qcow2',
1052
 
             '-b', backing_image_path, disk_image_path])
1053
 
        run_subprocess(
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')
 
1131
 
 
1132
 
 
1133
def lxc_info(vm_name, source=None):
 
1134
    """Parse state info from the lxc-info output.
 
1135
 
 
1136
    :param vm_name: The vm we want to query about.
 
1137
 
 
1138
    :param source: A list of lines as produced by virsh list --all without
 
1139
        decorations (header/footer).
 
1140
    """
 
1141
    if source is None:
 
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)
 
1148
 
 
1149
 
 
1150
class Lxc(VM):
 
1151
 
 
1152
    def __init__(self, conf):
 
1153
        super(Lxc, self).__init__(conf)
 
1154
        self._guest_seed_path = None
 
1155
        self._fstab_path = None
 
1156
 
 
1157
    def state(self):
 
1158
        info = lxc_info(self.conf.get('vm.name'))
 
1159
        return info['state']
 
1160
 
 
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
 
1165
        return True
 
1166
 
 
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))
 
1181
 
 
1182
    def install(self):
 
1183
        '''Create an lxc, relying on cloud-init to customize the base image.
 
1184
 
 
1185
        There are two processes involvded here:
 
1186
        - lxc-create creates the vm.
 
1187
        - progress is monitored via the console to detect cloud-final.
 
1188
 
 
1189
        Once cloud-init has finished, the vm can be powered off.
 
1190
        '''
 
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
 
1209
            self._config_dir,
 
1210
            '%s.console' % (self.conf.get('vm.name'),))
 
1211
        # Create/empty the file so we get access to it (otherwise it will be
 
1212
        # owned by root).
 
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
 
1216
        run_subprocess(
 
1217
            ['sudo', 'lxc-create',
 
1218
             '-n', self.conf.get('vm.name'),
 
1219
             '-t', 'ubuntu-cloud',
 
1220
             '--',
 
1221
             '-r', self.conf.get('vm.release'),
 
1222
             '-a', self.conf.get('vm.cpu_model'),
 
1223
             '-C',  # From cloud image, implying download/cache
 
1224
             ])
 
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
 
1235
                     '-d',
 
1236
                     ]
 
1237
        run_subprocess(lxc_start)
 
1238
        self.parse_console_during_install(lxc_start)
 
1239
 
 
1240
    def poweroff(self):
 
1241
        return run_subprocess(
 
1242
            ['sudo', 'lxc-stop', '-n', self.conf.get('vm.name')])
 
1243
 
 
1244
    def undefine(self):
 
1245
        try:
 
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.
 
1253
                pass
 
1254
            else:
 
1255
                raise
 
1256
 
 
1257
 
 
1258
vm_class_registry.register('lxc', Lxc, 'Linux container virtual machine')
1057
1259
 
1058
1260
 
1059
1261
class ArgParser(argparse.ArgumentParser):
1108
1310
class Download(Command):
1109
1311
 
1110
1312
    def run(self):
1111
 
        # FIXME: what needs to be downloaded should depend on the type of the
1112
 
        # vm (possibly errors if there is nothing to download). -- vila
1113
 
        # 2013-02-06
1114
 
        self.vm.download_cloud_image(force=True)
 
1313
        self.vm.download(force=True)
1115
1314
 
1116
1315
 
1117
1316
class SshKeyGen(Command):
1124
1323
 
1125
1324
    def run(self):
1126
1325
        vm_name = self.vm.conf.get('vm.name')
1127
 
        state = vm_states().get(vm_name, None)
1128
 
        if state == 'shut off':
 
1326
        state = self.vm.state()
 
1327
        if state in('shut off', 'STOPPED'):
1129
1328
            self.vm.undefine()
1130
 
        elif state == 'running':
 
1329
        elif state in ('running', 'RUNNING'):
1131
1330
            raise SetupVmError('{name} is running', name=vm_name)
1132
 
        # FIXME: The installation method may vary depending on the vm type.
1133
 
        # -- vila 2013-02-06
1134
1331
        self.vm.install()
1135
1332
 
1136
1333
 
1142
1339
    ns = arg_parser.parse_args(args, out=out, err=err)
1143
1340
 
1144
1341
    conf = VmStack(ns.name)
1145
 
    with_backing = conf.get('vm.backing')
1146
 
    if with_backing is None:
1147
 
        vm = KvmFromCloudImage(conf)
1148
 
    else:
1149
 
        vm = KvmFromBacking(conf)
 
1342
    vm = conf.get('vm.class')(conf)
1150
1343
    if ns.download:
1151
1344
        cmds.append(Download(vm))
1152
1345
    if ns.ssh_keygen: