Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 510 lines (410 sloc) 14.471 kB
ff9c06b Initial commit
Patrick Devine authored
1
2 ###############################################################################
3 # Copyright (c) 2008-2009 VMware, Inc.
4 #
5 # This file is part of Weasel.
6 #
7 # Weasel is free software; you can redistribute it and/or modify it under
8 # the terms of the GNU General Public License as published by the Free
9 # Software Foundation version 2 and no later version.
10 #
11 # Weasel is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
14 # version 2 for more details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # this program; if not, write to the Free Software Foundation, Inc., 51
18 # Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
19 #
20
21 import re
22 import os
23 import glob
24 import stat
25 import util
26 import string
27 import shutil
28 import consts
29
30 import migration
31
32 from log import log
33
34 # TODO: Any required files need to be added to the precheck.
35
36 PATHS_TO_MIGRATE_PRE_PACKAGES = [
37 "/etc/vmware",
38 ]
39
40 # Names of files and directories to copy from the old installation to the new.
41 PATHS_TO_MIGRATE = [
42 "/etc/logrotate.conf",
43
44 "/etc/localtime",
45 "/etc/ntp.conf",
46
47 "/etc/syslog.conf",
48
49 "/etc/sysconfig/ntpd",
50 "/etc/sysconfig/xinetd",
51 "/etc/sysconfig/console",
52 "/etc/sysconfig/i18n",
53 "/etc/sysconfig/clock",
54 "/etc/sysconfig/crond",
55 "/etc/sysconfig/syslog",
56 "/etc/sysconfig/keyboard",
57 "/etc/sysconfig/mouse",
58
59 "/etc/ssh",
60
61 "/etc/nsswitch.conf",
62 "/etc/yp.conf",
63 "/etc/krb.conf",
64 "/etc/krb.realms",
65 "/etc/krb5.conf",
66 "/etc/login.defs",
67
68 "/etc/pam.d/*",
69
70 "/etc/hosts.allow",
71 "/etc/hosts.deny",
72
73 "/etc/ldap.conf",
74 "/etc/openldap",
75
76 "/etc/sudoers",
77
78 "/etc/snmp",
79
80 "/usr/local/etc", # ntp stores a bunch of stuff in here.
81
82 "/etc/rc.d/rc*.d/*",
83 "/etc/xinetd.conf",
84
85 "/etc/motd",
86 # SW iSCSI Configuration
87 "/etc/initiatorname.vmkiscsi",
88 "/etc/vmkiscsi.conf",
89 ]
90
91 CMDS_TO_RUN = [
92 "/usr/sbin/tzdata-update",
93 ]
94
95 class NamedList(list):
96 '''Extension of the list type that lets you reference indexes by name,
97 which is useful for processing formatted text configuration files.
98
99 This class is used in conjunction with TextConfigFile classes to represent
100 individual lines in the config file.
101
102 >>> class MyNamedList(NamedList):
103 ... STRUCT = ['first', 'second']
104 >>> l = MyNamedList(['a', 'b'])
105 >>> l.first
106 'a'
107 >>> l.second
108 'b'
109 '''
110
111 STRUCT = [] # Set by subclass to the list of attribute names for each index
112
113 def __getattr__(self, key):
114 if key not in self.STRUCT:
115 raise AttributeError(key)
116
117 return self[self.STRUCT.index(key)]
118
119 def __setattr__(self, key, value):
120 if key not in self.STRUCT:
121 return list.__setattr__(self, key, value)
122
123 self[self.STRUCT.index(key)] = value
124
125 class TextConfigFile:
126 '''Base class for text-based configuration files that are line-oriented.
127
128 Many unix configuration files, like /etc/passwd and /etc/group, are made up
129 of individual lines separated by a token. This class, along with NamedList,
130 provide a common means to load and process these files.
131
132 >>> import sys
133 >>> log.warn = sys.stdout.write
134 >>> class MyConfig(TextConfigFile):
135 ... class MyConfigData(NamedList):
136 ... STRUCT = ['first', 'second']
137 ... ELEMENT_TYPE = MyConfigData
138 >>> # Construct a MyConfig object from a three line config file.
139 >>> mc = MyConfig("""
140 ... a b
141 ... c d
142 ... this will not match
143 ... """)
144 skipping config line -- this will not match
145 >>> mc.elements
146 [['a', 'b'], ['c', 'd']]
147 '''
148
149 # Optionally set by subclass to the separator string for a line.
150 SEPARATOR = r'\s+'
151
152 # Set by the TextConfigFile subclass to the NamedList subclass that will
153 # represent each line found in the file.
154 ELEMENT_TYPE = NamedList
155
156 @classmethod
157 def fromFile(cls, path):
158 '''Construct an object of the class type with the contents of the given
159 file path.'''
160
161 fh = open(path)
162 contents = fh.read()
163 fh.close()
164
165 return cls(contents)
166
167 def __init__(self, contentString):
168 '''Construct an config file object from the given contents.
169
170 The contents are split into individual lines and then split into
171 individual fields based on the SEPARATOR regex. If the number of fields
172 matches the length of the ELEMENT_TYPE.STRUCT list, a new element of
173 ELEMENT_TYPE will be constructed with the fields.
174 '''
175
176 self.elements = []
177
178 for line in map(string.strip, contentString.split('\n')):
179 if not line:
180 continue
181
182 rawFields = re.split(self.SEPARATOR, line)
183 fields = self._addDefaultFields(rawFields)
184 if len(fields) == len(self.ELEMENT_TYPE.STRUCT):
185 self.elements.append(self.ELEMENT_TYPE(fields))
186 else:
187 log.warn("skipping config line -- %s" % line)
188
189 def __getitem__(self, key):
190 return self.elements[key]
191
192 def _addDefaultFields(self, fields):
193 return fields
194
195 def cloneFile(src, dst):
196 '''Clone a file, including its owner ids.'''
197 log.debug("cloning file %s -> %s" % (src, dst))
198 dirs = os.path.dirname(dst)
199 if not os.path.exists(dirs):
200 os.makedirs(dirs)
201 preserveFile(dst)
202 shutil.copy2(src, dst)
203 st = os.stat(src)
204 os.chown(dst, st[stat.ST_UID], st[stat.ST_GID])
205
206 def cloneDir(src, dst):
207 log.debug("cloning dir %s -> %s" % (src, dst))
208 if not os.path.exists(dst):
209 os.makedirs(dst)
210 st = os.stat(src)
211 shutil.copystat(src, dst)
212 os.chown(dst, st[stat.ST_UID], st[stat.ST_GID])
213
214 def cloneLink(src, dst):
215 log.debug("cloning link %s -> %s" % (src, dst))
216 if os.path.lexists(dst):
217 os.remove(dst)
218 st = os.lstat(src)
219 linkDst = os.readlink(src)
220 os.symlink(linkDst, dst)
221 os.lchown(dst, st[stat.ST_UID], st[stat.ST_GID])
222
223 def cloneTree(src, dst):
224 log.debug("cloning tree %s -> %s" % (src, dst))
225 for root, dirs, files in os.walk(src):
226 subDirs = root[len(src):].lstrip('/')
227 for name in dirs:
228 cloneDir(os.path.join(root, name),
229 os.path.join(dst, subDirs, name))
230 for name in files:
231 clonePath(os.path.join(root, name),
232 os.path.join(dst, subDirs, name))
233
234 def clonePath(src, dst):
235 if os.path.isdir(src):
236 cloneTree(src, dst)
237 elif os.path.islink(src):
238 cloneLink(src, dst)
239 else:
240 cloneFile(src, dst)
241
242 def resolveLink(prefix, path):
243 '''Resolve a sym link in the ESX3 file system.'''
244 count = 0
245 while os.path.islink(path):
246 # We have to resolve links ourselves instead of using os.path.realpath
247 # because we need to prefix '/mnt/sysimage/esx3-installation' onto any
248 # absolute paths.
249 linkPath = os.readlink(path)
250 if not os.path.isabs(linkPath):
251 path = os.path.join(os.path.dirname(path), linkPath)
252 else:
253 # Absolute links should be made relative to the esx3 mount in the
254 # installer.
255 path = os.path.join(prefix, linkPath.lstrip('/'))
256 path = os.path.normpath(path)
257 if not os.path.lexists(path):
258 log.warn("link destination does not exist -- %s" % path)
259 return None
260 count += 1
261 if count > 10:
262 log.warn("sym link loop")
263 return None
264 return path
265
266 def expandGlobPath(path):
267 '''Expand a given path into a list of files to be migrated.
268
269 The path can be a shell glob that specifies either files or directories.
270 For a file, if there is a custom handler in migration.MIGRATION_HANDLERS,
271 it will be called to scan the file and perform any migration work. If
272 the migration handler discovers any other files referenced they will be
273 added to the list returned by this function.
274
275 If the path is a directory, its contents will be walked and added to the
276 list of files returned.
277 '''
278 retval = []
279
280 prefix = consts.HOST_ROOT + consts.ESX3_INSTALLATION.lstrip('/')
281 oldPathGlob = prefix + path
282 paths = glob.glob(oldPathGlob)
283 for oldPath in paths:
284 strippedPath = oldPath[len(prefix):]
285 newPath = os.path.join(consts.HOST_ROOT, strippedPath.lstrip('/'))
286
287 if path in migration.MIGRATION_HANDLERS:
288 log.debug("custom migration handler -- %s" % path)
289 handler = migration.MIGRATION_HANDLERS[path]
290 accum = []
291 # If the oldPath is a link, we need to resolve it so that the
292 # custom handler can open the file up.
293 actualOldPath = resolveLink(consts.HOST_ROOT +
294 consts.ESX3_INSTALLATION.lstrip('/'),
295 oldPath)
296 if not actualOldPath:
297 log.warn(" skipping unresolved sym link -- %s" % oldPath)
298 continue
299 doClone = handler(actualOldPath, newPath, accum)
300
301 # Need to recursively expand paths returned by the migration
302 # handler in case they need processing as well. For example, if
303 # a config file includes another config file, the second file will
304 # need to be run through the custom migration handler as well.
305 for extraPath in accum:
306 retval.extend(expandGlobPath(extraPath))
307
308 if not doClone:
309 log.debug("custom handler migrated file -- %s" % path)
310 continue
311
312 retval.append(path)
313 if os.path.isdir(oldPath):
314 for name in os.listdir(oldPath):
315 subdir = oldPath[
316 len(consts.HOST_ROOT + consts.ESX3_INSTALLATION.lstrip('/')):]
317 retval.extend(expandGlobPath(os.path.join(subdir, name)))
318
319 return retval
320
321 def migratePath(path):
322 '''Attempt to migrate a file from the ESX v3 installation to the new one.'''
323
324 allPaths = expandGlobPath(path)
325 if not allPaths:
326 log.info("not migrating globbed path -- %s" % path)
327
328 for expandedPath in allPaths:
329 oldPath = consts.HOST_ROOT + consts.ESX3_INSTALLATION + expandedPath
330 newPath = consts.HOST_ROOT + expandedPath
331
332 if not os.path.exists(oldPath) and not os.path.islink(oldPath):
333 log.info("not migrating expanded path '%s' from '%s'" % (
334 expandedPath, path))
335 continue
336
337 if os.path.isdir(oldPath):
338 # The expandGlobPath has already walked down the tree, so we just
339 # want to clone the directory's permissions and what not.
340 cloneDir(oldPath, newPath)
341 else:
342 clonePath(oldPath, newPath)
343
344 def preserveFile(path):
345 '''Preserve a file in the new installation and return its name or None if
346 the file does not exist.'''
347
348 retval = None
349 if os.path.exists(path) and not path.endswith(".esx4"):
350 retval = path + ".esx4"
351 cloneFile(path, retval)
352 return retval
353
354 def hostActionPrePackages(_context):
355 for path in PATHS_TO_MIGRATE_PRE_PACKAGES:
356 migratePath(path)
357
358 def hostAction(_context):
359 for path in PATHS_TO_MIGRATE:
360 migratePath(path)
361
362 for cmd in CMDS_TO_RUN:
363 util.execCommand(cmd, root=consts.HOST_ROOT)
364
365 CLEANUP_TEMPLATE = """\
366 #! /bin/sh
367
368 usage()
369 {
370 echo "usage: $0 [-hf]"
371 echo "Removes references to ESX v3 in grub.conf and /etc/fstab."
372 echo "Also removes ESX v3 files in /boot."
373 echo
374 echo "Options:"
375 echo " -h Show this help message."
376 echo " -f Force, run the script in non-interactive mode."
377 exit 0
378 }
379
380 exitmsg()
381 {
382 if test $? -eq 0; then
383 echo "Cleanup of ESX v3 successful. Please reboot your system."
384 fi
385 }
386
387 args=`getopt hf $*`
388
389 if test $? != 0; then
390 usage
391 fi
392
393 set -- $args
394
395 force="no"
396
397 for i
398 do
399 case "$i" in
400 -h)
401 usage
402 ;;
403 -f)
404 force="yes"
405 ;;
406 esac
407 done
408
409 if test $force != "yes"; then
410 read -p "Are you sure you want to remove ESX v3 references and files? (y/N) " answer
411 if test "$answer" != "y" && test "$answer" != "yes"; then
412 exit 0
413 fi
414 fi
415
416 trap exitmsg EXIT
417
418 rm /usr/sbin/rollback-to-esx3
419 sed -i -e '/^# BEGIN migrated entries/,/^# END migrated entries/d' /etc/fstab
420
421 """
422
423 ROLLBACK_TEMPLATE = """\
424 #! /bin/sh
425
426 usage()
427 {
428 echo "usage: $0 [-hf]"
429 echo "Reconfigure the bootloader to boot into ESX v3 on the next reboot."
430 echo
431 echo "Options:"
432 echo " -h Show this help message."
433 echo " -f Force, run the script in non-interactive mode."
434 exit 0
435 }
436
437 exitmsg()
438 {
439 if test $? -eq 0; then
440 echo "Rollback to ESXv3 successful. Please reboot your system."
441 fi
442 }
443
444 args=`getopt hf $*`
445
446 if test $? != 0; then
447 usage
448 fi
449
450 set -- $args
451
452 force="no"
453
454 for i
455 do
456 case "$i" in
457 -h)
458 usage
459 ;;
460 -f)
461 force="yes"
462 ;;
463 esac
464 done
465
466 if test $force != "yes"; then
467 echo "Warning: Any changes made to the virtual machines on this host will"
468 echo "not be rolled back. If you upgraded the virtual hardware of the"
469 echo "machines, they will not work after the rollback."
470 read -p "Are you sure you want to rollback to ESX v3? (y/N) " answer
471 if test "$answer" != "y" && test "$answer" != "yes"; then
472 exit 0
473 fi
474 fi
475
476 trap exitmsg EXIT
477
478 rm -rf /boot/config-2.6.*
479 rm -rf /boot/initrd-2.6.*
480 rm -rf /boot/initrd.img
481 rm -rf /boot/System.map-2.6.*
482 rm -rf /boot/vmlinuz-2.6.*
483 rm -rf /boot/vmlinuz
484 rm -rf /boot/trouble
485 cp /boot/grub/grub.conf.esx3 /boot/grub/grub.conf
486
487 """
488
489 CLEANUP_PATH = os.path.join(consts.HOST_ROOT, "usr/sbin/cleanup-esx3")
490 ROLLBACK_PATH = os.path.join(consts.HOST_ROOT, "usr/sbin/rollback-to-esx3")
491
492 def hostActionCleanupScripts(_context):
493 '''Write out the scripts used to cleanup the old installation or rollback
494 to it.'''
495 fp = open(CLEANUP_PATH, "w")
496 fp.write(CLEANUP_TEMPLATE)
497 fp.close()
498
499 os.chmod(CLEANUP_PATH, 0700)
500
501 fp = open(ROLLBACK_PATH, "w")
502 fp.write(ROLLBACK_TEMPLATE)
503 fp.close()
504
505 os.chmod(ROLLBACK_PATH, 0700)
506
507 if __name__ == "__main__":
508 import doctest
509 doctest.testmod()
Something went wrong with that request. Please try again.