OpenStack in a snap!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

550 lines
18 KiB

  1. """questions.py
  2. All of our subclasses of Question live here.
  3. We might break this file up into multiple pieces at some point, but
  4. for now, we're keeping things simple (if a big lengthy)
  5. ----------------------------------------------------------------------
  6. Copyright 2019 Canonical Ltd
  7. Licensed under the Apache License, Version 2.0 (the "License");
  8. you may not use this file except in compliance with the License.
  9. You may obtain a copy of the License at
  10. http://www.apache.org/licenses/LICENSE-2.0
  11. Unless required by applicable law or agreed to in writing, software
  12. distributed under the License is distributed on an "AS IS" BASIS,
  13. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. See the License for the specific language governing permissions and
  15. limitations under the License.
  16. """
  17. from time import sleep
  18. from os import path
  19. from init.shell import (check, call, check_output, shell, sql, nc_wait,
  20. log_wait, restart, download)
  21. from init.config import Env, log
  22. from init.question import Question
  23. _env = Env().get_env()
  24. class ConfigError(Exception):
  25. """Suitable error to raise in case there is an issue with the snapctl
  26. config or environment vars.
  27. """
  28. class ConfigQuestion(Question):
  29. """Question class that simply asks for and sets a config value.
  30. All we need to do is run 'snap-openstack setup' after we have saved
  31. off the value. The value to be set is specified by the name of the
  32. question class.
  33. """
  34. def after(self, answer):
  35. """Our value has been saved.
  36. Run 'snap-openstack setup' to write it out, and load any changes to
  37. microstack.rc.
  38. # TODO this is a bit messy and redundant. Come up with a clean
  39. way of loading and writing config after the run of
  40. ConfigQuestions have been asked.
  41. """
  42. check('snap-openstack', 'setup')
  43. mstackrc = '{SNAP_COMMON}/etc/microstack.rc'.format(**_env)
  44. with open(mstackrc, 'r') as rc_file:
  45. for line in rc_file.readlines():
  46. if not line.startswith('export'):
  47. continue
  48. key, val = line[7:].split('=')
  49. _env[key.strip()] = val.strip()
  50. class Dns(Question):
  51. """Possibly override default dns."""
  52. _type = 'string'
  53. _question = 'DNS to use'
  54. def yes(self, answer: str):
  55. """Override the default dhcp_agent.ini file."""
  56. file_path = '{SNAP_COMMON}/etc/neutron/dhcp_agent.ini'.format(**_env)
  57. with open(file_path, 'w') as f:
  58. f.write("""\
  59. [DEFAULT]
  60. interface_driver = openvswitch
  61. dhcp_driver = neutron.agent.linux.dhcp.Dnsmasq
  62. enable_isolated_metadata = True
  63. dnsmasq_dns_servers = {answer}
  64. """.format(answer=answer))
  65. # Neutron is not actually started at this point, so we don't
  66. # need to restart.
  67. # TODO: This isn't idempotent, because it will behave
  68. # differently if we re-run this script when neutron *is*
  69. # started. Need to figure that out.
  70. class ExtGateway(ConfigQuestion):
  71. """Possibly override default ext gateway."""
  72. _type = 'string'
  73. _question = 'External Gateway'
  74. def yes(self, answer):
  75. # Preserve old behavior.
  76. # TODO: update this
  77. _env['extgateway'] = answer
  78. class ExtCidr(ConfigQuestion):
  79. """Possibly override the cidr."""
  80. _type = 'string'
  81. _question = 'External Ip Range'
  82. def yes(self, answer):
  83. # Preserve old behavior.
  84. # TODO: update this
  85. _env['extcidr'] = answer
  86. class OsPassword(ConfigQuestion):
  87. _type = 'string'
  88. _question = 'Openstack Admin Password'
  89. def yes(self, answer):
  90. # Preserve old behavior.
  91. # TODO: update this
  92. _env['ospassword'] = answer
  93. # TODO obfuscate the password!
  94. class IpForwarding(Question):
  95. """Possibly setup IP forwarding."""
  96. _type = 'boolean' # Auto for now, to maintain old behavior.
  97. _question = 'Do you wish to setup ip forwarding? (recommended)'
  98. def yes(self, answer: str) -> None:
  99. """Use sysctl to setup ip forwarding."""
  100. log.info('Setting up ipv4 forwarding...')
  101. check('sysctl', 'net.ipv4.ip_forward=1')
  102. class ForceQemu(Question):
  103. _type = 'auto'
  104. def yes(self, answer: str) -> None:
  105. """Possibly force us to use qemu emulation rather than kvm."""
  106. cpuinfo = check_output('cat', '/proc/cpuinfo')
  107. if 'vmx' in cpuinfo or 'svm' in cpuinfo:
  108. # We have processor extensions installed. No need to Force
  109. # Qemu emulation.
  110. return
  111. _path = '{SNAP_COMMON}/etc/nova/nova.conf.d/hypervisor.conf'.format(
  112. **_env)
  113. with open(_path, 'w') as _file:
  114. _file.write("""\
  115. [DEFAULT]
  116. compute_driver = libvirt.LibvirtDriver
  117. [workarounds]
  118. disable_rootwrap = True
  119. [libvirt]
  120. virt_type = qemu
  121. cpu_mode = host-model
  122. """)
  123. # TODO: restart nova services when re-running this after init.
  124. class VmSwappiness(Question):
  125. _type = 'boolean'
  126. _question = 'Do you wish to set vm swappiness to 1? (recommended)'
  127. def yes(self, answer: str) -> None:
  128. # TODO
  129. pass
  130. class FileHandleLimits(Question):
  131. _type = 'boolean'
  132. _question = 'Do you wish to increase file handle limits? (recommended)'
  133. def yes(self, answer: str) -> None:
  134. # TODO
  135. pass
  136. class RabbitMq(Question):
  137. """Wait for Rabbit to start, then setup permissions."""
  138. _type = 'boolean'
  139. def _wait(self) -> None:
  140. nc_wait(_env['extgateway'], '5672')
  141. log_file = '{SNAP_COMMON}/log/rabbitmq/startup_log'.format(**_env)
  142. log_wait(log_file, 'completed')
  143. def _configure(self) -> None:
  144. """Configure RabbitMQ
  145. (actions may have already been run, in which case we fail silently).
  146. """
  147. # Add Erlang HOME to env.
  148. env = dict(**_env)
  149. env['HOME'] = '{SNAP_COMMON}/lib/rabbitmq'.format(**_env)
  150. # Configure RabbitMQ
  151. call('rabbitmqctl', 'add_user', 'openstack', 'rabbitmq', env=env)
  152. shell('rabbitmqctl set_permissions openstack ".*" ".*" ".*"', env=env)
  153. def yes(self, answer: str) -> None:
  154. log.info('Waiting for RabbitMQ to start ...')
  155. self._wait()
  156. log.info('RabbitMQ started!')
  157. log.info('Configuring RabbitMQ ...')
  158. self._configure()
  159. log.info('RabbitMQ Configured!')
  160. class DatabaseSetup(Question):
  161. """Setup keystone permissions, then setup all databases."""
  162. _type = 'boolean'
  163. def _wait(self) -> None:
  164. nc_wait(_env['extgateway'], '3306')
  165. log_wait('{SNAP_COMMON}/log/mysql/error.log'.format(**_env),
  166. 'mysqld: ready for connections.')
  167. def _create_dbs(self) -> None:
  168. for db in ('neutron', 'nova', 'nova_api', 'nova_cell0', 'cinder',
  169. 'glance', 'keystone'):
  170. sql("CREATE DATABASE IF NOT EXISTS {db};".format(db=db))
  171. sql(
  172. "GRANT ALL PRIVILEGES ON {db}.* TO {db}@{extgateway} \
  173. IDENTIFIED BY '{db}';".format(db=db, **_env))
  174. def _bootstrap(self) -> None:
  175. if call('openstack', 'user', 'show', 'admin'):
  176. return
  177. bootstrap_url = 'http://{extgateway}:5000/v3/'.format(**_env)
  178. check('snap-openstack', 'launch', 'keystone-manage', 'bootstrap',
  179. '--bootstrap-password', _env['ospassword'],
  180. '--bootstrap-admin-url', bootstrap_url,
  181. '--bootstrap-internal-url', bootstrap_url,
  182. '--bootstrap-public-url', bootstrap_url)
  183. def yes(self, answer: str) -> None:
  184. """Setup Databases.
  185. Create all the MySQL databases we require, then setup the
  186. fernet keys and create the service project.
  187. """
  188. log.info('Waiting for MySQL server to start ...')
  189. self._wait()
  190. log.info('Mysql server started! Creating databases ...')
  191. self._create_dbs()
  192. check('snapctl', 'set', 'database.ready=true')
  193. # Start keystone-uwsgi. We use snapctl, because systemd
  194. # doesn't yet know about the service.
  195. check('snapctl', 'start', 'microstack.nginx')
  196. check('snapctl', 'start', 'microstack.keystone-uwsgi')
  197. log.info('Configuring Keystone Fernet Keys ...')
  198. check('snap-openstack', 'launch', 'keystone-manage',
  199. 'fernet_setup', '--keystone-user', 'root',
  200. '--keystone-group', 'root')
  201. check('snap-openstack', 'launch', 'keystone-manage', 'db_sync')
  202. restart('keystone-*')
  203. log.info('Bootstrapping Keystone ...')
  204. self._bootstrap()
  205. log.info('Creating service project ...')
  206. if not call('openstack', 'project', 'show', 'service'):
  207. check('openstack', 'project', 'create', '--domain',
  208. 'default', '--description', 'Service Project',
  209. 'service')
  210. log.info('Keystone configured!')
  211. class NovaSetup(Question):
  212. """Create all relevant nova users and services."""
  213. _type = 'boolean'
  214. def _flavors(self) -> None:
  215. """Create default flavors."""
  216. if not call('openstack', 'flavor', 'show', 'm1.tiny'):
  217. check('openstack', 'flavor', 'create', '--id', '1',
  218. '--ram', '512', '--disk', '1', '--vcpus', '1', 'm1.tiny')
  219. if not call('openstack', 'flavor', 'show', 'm1.small'):
  220. check('openstack', 'flavor', 'create', '--id', '2',
  221. '--ram', '2048', '--disk', '20', '--vcpus', '1', 'm1.small')
  222. if not call('openstack', 'flavor', 'show', 'm1.medium'):
  223. check('openstack', 'flavor', 'create', '--id', '3',
  224. '--ram', '4096', '--disk', '20', '--vcpus', '2', 'm1.medium')
  225. if not call('openstack', 'flavor', 'show', 'm1.large'):
  226. check('openstack', 'flavor', 'create', '--id', '4',
  227. '--ram', '8192', '--disk', '20', '--vcpus', '4', 'm1.large')
  228. if not call('openstack', 'flavor', 'show', 'm1.xlarge'):
  229. check('openstack', 'flavor', 'create', '--id', '5',
  230. '--ram', '16384', '--disk', '20', '--vcpus', '8',
  231. 'm1.xlarge')
  232. def yes(self, answer: str) -> None:
  233. log.info('Configuring nova ...')
  234. if not call('openstack', 'user', 'show', 'nova'):
  235. check('openstack', 'user', 'create', '--domain',
  236. 'default', '--password', 'nova', 'nova')
  237. check('openstack', 'role', 'add', '--project',
  238. 'service', '--user', 'nova', 'admin')
  239. if not call('openstack', 'user', 'show', 'placement'):
  240. check('openstack', 'user', 'create', '--domain', 'default',
  241. '--password', 'placement', 'placement')
  242. check('openstack', 'role', 'add', '--project', 'service',
  243. '--user', 'placement', 'admin')
  244. if not call('openstack', 'service', 'show', 'compute'):
  245. check('openstack', 'service', 'create', '--name', 'nova',
  246. '--description', '"Openstack Compute"', 'compute')
  247. for endpoint in ['public', 'internal', 'admin']:
  248. call('openstack', 'endpoint', 'create', '--region',
  249. 'microstack', 'compute', endpoint,
  250. 'http://{extgateway}:8774/v2.1'.format(**_env))
  251. if not call('openstack', 'service', 'show', 'placement'):
  252. check('openstack', 'service', 'create', '--name',
  253. 'placement', '--description', '"Placement API"',
  254. 'placement')
  255. for endpoint in ['public', 'internal', 'admin']:
  256. call('openstack', 'endpoint', 'create', '--region',
  257. 'microstack', 'placement', endpoint,
  258. 'http://{extgateway}:8778'.format(**_env))
  259. # Grant nova user access to cell0
  260. sql(
  261. "GRANT ALL PRIVILEGES ON nova_cell0.* TO 'nova'@'{extgateway}' \
  262. IDENTIFIED BY \'nova';".format(**_env))
  263. # Use snapctl to start nova services. We need to call them
  264. # out manually, because systemd doesn't know about them yet.
  265. # TODO: parse the output of `snapctl services` to get this
  266. # list automagically.
  267. for service in [
  268. 'microstack.nova-api',
  269. 'microstack.nova-api-metadata',
  270. 'microstack.nova-compute',
  271. 'microstack.nova-conductor',
  272. 'microstack.nova-scheduler',
  273. 'microstack.nova-uwsgi',
  274. ]:
  275. check('snapctl', 'start', service)
  276. check('snap-openstack', 'launch', 'nova-manage', 'api_db', 'sync')
  277. if 'cell0' not in check_output('snap-openstack', 'launch',
  278. 'nova-manage', 'cell_v2',
  279. 'list_cells'):
  280. check('snap-openstack', 'launch', 'nova-manage',
  281. 'cell_v2', 'map_cell0')
  282. if 'cell1' not in check_output('snap-openstack', 'launch',
  283. 'nova-manage', 'cell_v2', 'list_cells'):
  284. check('snap-openstack', 'launch', 'nova-manage', 'cell_v2',
  285. 'create_cell', '--name=cell1', '--verbose')
  286. check('snap-openstack', 'launch', 'nova-manage', 'db', 'sync')
  287. restart('nova-*')
  288. nc_wait(_env['extgateway'], '8774')
  289. sleep(5) # TODO: log_wait
  290. log.info('Creating default flavors...')
  291. self._flavors()
  292. class NeutronSetup(Question):
  293. """Create all relevant neutron services and users."""
  294. _type = 'boolean'
  295. def yes(self, answer: str) -> None:
  296. log.info('Configuring Neutron')
  297. if not call('openstack', 'user', 'show', 'neutron'):
  298. check('openstack', 'user', 'create', '--domain', 'default',
  299. '--password', 'neutron', 'neutron')
  300. check('openstack', 'role', 'add', '--project', 'service',
  301. '--user', 'neutron', 'admin')
  302. if not call('openstack', 'service', 'show', 'network'):
  303. check('openstack', 'service', 'create', '--name', 'neutron',
  304. '--description', '"OpenStack Network"', 'network')
  305. for endpoint in ['public', 'internal', 'admin']:
  306. call('openstack', 'endpoint', 'create', '--region',
  307. 'microstack', 'network', endpoint,
  308. 'http://{extgateway}:9696'.format(**_env))
  309. for service in [
  310. 'microstack.neutron-api',
  311. 'microstack.neutron-dhcp-agent',
  312. 'microstack.neutron-l3-agent',
  313. 'microstack.neutron-metadata-agent',
  314. 'microstack.neutron-openvswitch-agent',
  315. ]:
  316. check('snapctl', 'start', service)
  317. check('snap-openstack', 'launch', 'neutron-db-manage', 'upgrade',
  318. 'head')
  319. restart('neutron-*')
  320. nc_wait(_env['extgateway'], '9696')
  321. sleep(5) # TODO: log_wait
  322. if not call('openstack', 'network', 'show', 'test'):
  323. check('openstack', 'network', 'create', 'test')
  324. if not call('openstack', 'subnet', 'show', 'test-subnet'):
  325. check('openstack', 'subnet', 'create', '--network', 'test',
  326. '--subnet-range', '192.168.222.0/24', 'test-subnet')
  327. if not call('openstack', 'network', 'show', 'external'):
  328. check('openstack', 'network', 'create', '--external',
  329. '--provider-physical-network=physnet1',
  330. '--provider-network-type=flat', 'external')
  331. if not call('openstack', 'subnet', 'show', 'external-subnet'):
  332. check('openstack', 'subnet', 'create', '--network', 'external',
  333. '--subnet-range', _env['extcidr'], '--no-dhcp',
  334. 'external-subnet')
  335. if not call('openstack', 'router', 'show', 'test-router'):
  336. check('openstack', 'router', 'create', 'test-router')
  337. check('openstack', 'router', 'add', 'subnet', 'test-router',
  338. 'test-subnet')
  339. check('openstack', 'router', 'set', '--external-gateway',
  340. 'external', 'test-router')
  341. class GlanceSetup(Question):
  342. """Setup glance, and download an initial Cirros image."""
  343. _type = 'boolean'
  344. def _fetch_cirros(self) -> None:
  345. if call('openstack', 'image', 'show', 'cirros'):
  346. return
  347. env = dict(**_env)
  348. env['VER'] = '0.4.0'
  349. env['IMG'] = 'cirros-{VER}-x86_64-disk.img'.format(**env)
  350. log.info('Fetching cirros image ...')
  351. cirros_path = '{SNAP_COMMON}/images/{IMG}'.format(**env)
  352. if not path.exists(cirros_path):
  353. check('mkdir', '-p', '{SNAP_COMMON}/images'.format(**env))
  354. download(
  355. 'http://download.cirros-cloud.net/{VER}/{IMG}'.format(**env),
  356. '{SNAP_COMMON}/images/{IMG}'.format(**env))
  357. check('openstack', 'image', 'create', '--file',
  358. '{SNAP_COMMON}/images/{IMG}'.format(**env),
  359. '--public', '--container-format=bare',
  360. '--disk-format=qcow2', 'cirros')
  361. def yes(self, answer: str) -> None:
  362. log.info('Configuring Glance ...')
  363. if not call('openstack', 'user', 'show', 'glance'):
  364. check('openstack', 'user', 'create', '--domain', 'default',
  365. '--password', 'glance', 'glance')
  366. check('openstack', 'role', 'add', '--project', 'service',
  367. '--user', 'glance', 'admin')
  368. if not call('openstack', 'service', 'show', 'image'):
  369. check('openstack', 'service', 'create', '--name', 'glance',
  370. '--description', '"OpenStack Image"', 'image')
  371. for endpoint in ['internal', 'admin', 'public']:
  372. check('openstack', 'endpoint', 'create', '--region',
  373. 'microstack', 'image', endpoint,
  374. 'http://{extgateway}:9292'.format(**_env))
  375. for service in [
  376. 'microstack.glance-api',
  377. 'microstack.registry', # TODO rename this to glance-registery
  378. ]:
  379. check('snapctl', 'start', service)
  380. check('snap-openstack', 'launch', 'glance-manage', 'db_sync')
  381. restart('glance*')
  382. nc_wait(_env['extgateway'], '9292')
  383. sleep(5) # TODO: log_wait
  384. self._fetch_cirros()
  385. class PostSetup(Question):
  386. """Sneak in any additional cleanup, then set the initialized state."""
  387. def yes(self, answer: str) -> None:
  388. log.info('restarting libvirt and virtlogd ...')
  389. # This fixes an issue w/ logging not getting set.
  390. # TODO: fix issue.
  391. restart('*virt*')
  392. # Start horizon
  393. check('snapctl', 'start', 'microstack.horizon-uwsgi')
  394. check('snapctl', 'set', 'initialized=true')
  395. log.info('Complete. Marked microstack as initialized!')