upgrade.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/python3
  2. import re
  3. import sys
  4. import time
  5. import pylxd
  6. from pylxd import Client
  7. class ExecuteError(RuntimeError):
  8. def __init__(self, command, exit_code):
  9. self.command = command
  10. self.exit_code = exit_code
  11. def __str__(self):
  12. return 'Command "%s" exit code %d' % (' '.join(self.command), self.exit_code)
  13. def find_source_image(client, image):
  14. try:
  15. client.images.get_by_alias(image)
  16. return {'type': 'image', 'alias': image}
  17. except pylxd.exceptions.NotFound as e:
  18. if len(image) >= 12:
  19. client.images.get(image)
  20. return {'type': 'image', 'fingerprint': image}
  21. else:
  22. raise
  23. def copy_config(old, new):
  24. new.devices = old.devices
  25. new.description = old.description
  26. new_config = new.config
  27. for key, value in old.config.items():
  28. if key.endswith("hwaddr"):
  29. new_config[key] = value
  30. # for key, value in new_config.items():
  31. # print("%s %s" % (key, value))
  32. new.config = new_config
  33. def log_stdout(message):
  34. print(message, end='', flush=True)
  35. def log_stderr(message):
  36. print(message, end='', file=sys.stderr, flush=True)
  37. class Container:
  38. def __init__(self, container):
  39. self.container = container
  40. def __getattr__(self, name):
  41. return self.container.__getattribute__(name)
  42. def __setattr__(self, name, value):
  43. if name not in ('container'):
  44. self.container.__setattr__(name, value)
  45. else:
  46. super(Container, self).__setattr__(name, value)
  47. def execute_with_output(self, command, *args, **kwargs):
  48. extra_args={}
  49. if 'stderr_handler' not in kwargs:
  50. extra_args['stderr_handler'] = log_stderr
  51. (exit_code, stdout, stderr) = self.container.execute(command, *args, **kwargs, **extra_args)
  52. if exit_code != 0:
  53. raise ExecuteError(command, exit_code)
  54. return stdout
  55. def execute(self, command, *args, **kwargs):
  56. extra_args={}
  57. if 'stdout_handler' not in kwargs:
  58. extra_args['stdout_handler'] = log_stdout
  59. self.execute_with_output(command, *args, **kwargs, **extra_args)
  60. def execute_retry(self, command, retries, *args, **kwargs):
  61. for i in range(retries + 1):
  62. try:
  63. self.execute(command, *args, **kwargs)
  64. except ExecuteError as e:
  65. if i == retries:
  66. raise
  67. continue
  68. return
  69. raise NotImplementedError()
  70. def ping(self, dest):
  71. self.execute_retry(['ping', '-c', '1', '-q', dest], 2,
  72. stdout_handler=None)
  73. def sysupgrade_backup(self, ):
  74. return self.execute_with_output(['sysupgrade', '-b', '-'],
  75. encoding='raw', decode=False)
  76. def sysupgrade_restore(self, data):
  77. backup_file = '/tmp/lxd-upgrade.tar.gz'
  78. self.files.put(backup_file, data)
  79. self.execute(['sysupgrade', '-r', backup_file])
  80. def opkg_list_installed(self, ):
  81. return self.execute_with_output(['opkg', 'list-installed'])
  82. def opkg_update(self):
  83. print("Update")
  84. self.execute(['opkg', 'update'])
  85. def opkg_install(self, packages):
  86. print("Installing %s" % packages)
  87. self.execute(['opkg', 'install'] + packages)
  88. def opkg_remove(self, packages):
  89. print("Removing %s" % packages)
  90. self.execute(['opkg', 'remove'] + packages)
  91. def _package_set_from_str(self, s):
  92. print("_package_set_from_str ", type(s))
  93. old_list = s.split('\n')
  94. old_packages = []
  95. pat = re.compile(r'([\w\.\-]*?)[0-9][0-9a-f\.\-]*')
  96. i = 1
  97. for l in old_list:
  98. i = i + 1
  99. res = l.split(' ')
  100. if len(res) == 3:
  101. (name, _, version) = res
  102. if name.startswith('lib'):
  103. m = pat.match(name)
  104. if m:
  105. name = m[1]
  106. old_packages.append(name)
  107. return frozenset(old_packages)
  108. def package_set(self):
  109. return self._package_set_from_str(self.opkg_list_installed())
  110. def orig_package_set(self):
  111. return self._package_set_from_str(self.container.files.get('/etc/openwrt_manifest').decode('ascii'))
  112. def save_orig_package_set(self):
  113. self.execute(['sh', '-c', 'opkg list-installed > tee /etc/openwrt_manifest'])
  114. def usage(argv):
  115. print("Usage:", argv[0], "<old container> <new container> <image>")
  116. exit(1)
  117. def main(argv):
  118. is_allow_existing = False
  119. if len(argv) == 4:
  120. pos = 1
  121. else:
  122. usage(argv)
  123. old_name = argv[pos]; pos=pos+1
  124. new_name = argv[pos]; pos=pos+1
  125. new_image = argv[pos]; pos=pos+1
  126. client = Client()
  127. old = Container(client.containers.get(old_name))
  128. if old.status == 'Stopped':
  129. print("Start", old_name)
  130. old.start(wait=True)
  131. new_source = find_source_image(client, new_image)
  132. new_config = {'name': new_name, 'source': new_source, 'profiles': old.profiles}
  133. if is_allow_existing and client.containers.exists(new_name):
  134. new = Container(client.containers.get(new_name))
  135. else:
  136. print("Create", new_name, new_config)
  137. new = Container(client.containers.create(new_config, wait=True))
  138. if new.status == 'Stopped':
  139. print("Start", new_name)
  140. new.start(wait=True)
  141. # Can't use ujail on LXD instances
  142. new.opkg_remove(['procd-ujail'])
  143. print("Ping downloads.openwrt.org")
  144. new.ping('downloads.openwrt.org')
  145. print("Update package list")
  146. new.opkg_update()
  147. print("Build /etc/openwrt_manifest")
  148. new.save_orig_package_set()
  149. orig_set = old.orig_package_set()
  150. old_set = old.package_set()
  151. new_set = new.package_set()
  152. del_set = orig_set.difference(old_set)
  153. add_set = old_set.difference(orig_set)
  154. del_packages = list(del_set)
  155. del_packages.append('procd-ujail')
  156. add_packages = list(add_set.difference(['iw']))
  157. if len(del_packages) > 0:
  158. print("Remove", del_packages)
  159. new.opkg_remove(del_packages)
  160. else:
  161. print("No packages uninstalled")
  162. if len(add_packages) > 0:
  163. print("Install", add_packages)
  164. new.opkg_install(add_packages)
  165. else:
  166. print("No packages installed")
  167. print("Backup", old_name)
  168. backup_data = old.sysupgrade_backup()
  169. print("Restore", new_name)
  170. new.sysupgrade_restore(backup_data)
  171. print("Stop", old_name)
  172. old.stop(wait=True)
  173. print("Stop", new_name)
  174. new.stop(wait=True)
  175. print("Copy config")
  176. copy_config(old, new)
  177. new.save()
  178. print("Wait 2s")
  179. time.sleep(2)
  180. print("Start", new_name)
  181. new.start(wait=True)
  182. print("Finished")
  183. if __name__ == '__main__':
  184. main(sys.argv)