Juju Charm - Nova Cloud Controller
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.

nova_cc_hooks.py 41KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220
  1. #!/usr/bin/python
  2. #
  3. # Copyright 2016 Canonical Ltd
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  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. import os
  17. import shutil
  18. import sys
  19. import uuid
  20. from subprocess import (
  21. check_call,
  22. )
  23. from urlparse import urlparse
  24. from charmhelpers.core.hookenv import (
  25. Hooks,
  26. UnregisteredHookError,
  27. config,
  28. charm_dir,
  29. is_leader,
  30. is_relation_made,
  31. log,
  32. local_unit,
  33. DEBUG,
  34. ERROR,
  35. WARNING,
  36. relation_get,
  37. relation_ids,
  38. relation_set,
  39. related_units,
  40. open_port,
  41. unit_get,
  42. status_set,
  43. )
  44. from charmhelpers.core.host import (
  45. service_reload,
  46. service_pause,
  47. )
  48. from charmhelpers.fetch import (
  49. apt_install,
  50. apt_update,
  51. filter_installed_packages
  52. )
  53. from charmhelpers.contrib.openstack.utils import (
  54. config_value_changed,
  55. configure_installation_source,
  56. git_install_requested,
  57. openstack_upgrade_available,
  58. os_release,
  59. os_requires_version,
  60. sync_db_with_multi_ipv6_addresses,
  61. pausable_restart_on_change as restart_on_change,
  62. is_unit_paused_set,
  63. CompareOpenStackReleases,
  64. )
  65. from charmhelpers.contrib.openstack.neutron import (
  66. network_manager,
  67. )
  68. from nova_cc_context import (
  69. NeutronAPIContext,
  70. NovaCellContext,
  71. )
  72. from charmhelpers.contrib.peerstorage import (
  73. peer_retrieve,
  74. peer_echo,
  75. )
  76. from nova_cc_utils import (
  77. add_hosts_to_cell,
  78. auth_token_config,
  79. cmd_all_services,
  80. determine_endpoints,
  81. determine_packages,
  82. determine_ports,
  83. disable_package_apache_site,
  84. disable_services,
  85. do_openstack_upgrade,
  86. enable_services,
  87. git_install,
  88. is_api_ready,
  89. is_cellv2_init_ready,
  90. keystone_ca_cert_b64,
  91. migrate_nova_databases,
  92. placement_api_enabled,
  93. save_script_rc,
  94. services,
  95. ssh_compute_add,
  96. ssh_compute_remove,
  97. ssh_known_hosts_lines,
  98. ssh_authorized_keys_lines,
  99. register_configs,
  100. restart_map,
  101. update_cell_database,
  102. NOVA_CONF,
  103. console_attributes,
  104. service_guard,
  105. guard_map,
  106. get_topics,
  107. setup_ipv6,
  108. is_db_initialised,
  109. assess_status,
  110. update_aws_compat_services,
  111. serial_console_settings,
  112. )
  113. from charmhelpers.contrib.hahelpers.cluster import (
  114. get_hacluster_config,
  115. https,
  116. is_clustered,
  117. )
  118. from charmhelpers.contrib.openstack.ha.utils import (
  119. update_dns_ha_resource_params,
  120. )
  121. from charmhelpers.payload.execd import execd_preinstall
  122. from charmhelpers.contrib.openstack.ip import (
  123. canonical_url,
  124. PUBLIC, INTERNAL, ADMIN,
  125. resolve_address,
  126. )
  127. from charmhelpers.contrib.network.ip import (
  128. format_ipv6_addr,
  129. get_iface_for_address,
  130. get_netmask_for_address,
  131. is_ipv6,
  132. get_relation_ip,
  133. )
  134. from charmhelpers.contrib.openstack.context import ADDRESS_TYPES
  135. from charmhelpers.contrib.charmsupport import nrpe
  136. from charmhelpers.contrib.hardening.harden import harden
  137. try:
  138. FileNotFoundError
  139. except NameError:
  140. # python3 compatibility
  141. FileNotFoundError = OSError
  142. hooks = Hooks()
  143. CONFIGS = register_configs()
  144. COLO_CONSOLEAUTH = 'inf: res_nova_consoleauth grp_nova_vips'
  145. AGENT_CONSOLEAUTH = 'ocf:openstack:nova-consoleauth'
  146. AGENT_CA_PARAMS = 'op monitor interval="5s"'
  147. NOVA_CONSOLEAUTH_OVERRIDE = '/etc/init/nova-consoleauth.override'
  148. def leader_init_db_if_ready(skip_acl_check=False, skip_cells_restarts=False,
  149. db_rid=None, unit=None):
  150. """Initialise db if leader and db not yet intialised.
  151. NOTE: must be called from database context.
  152. """
  153. if not is_leader():
  154. log("Not leader - skipping db init", level=DEBUG)
  155. return
  156. if is_db_initialised():
  157. log("Database already initialised - skipping db init", level=DEBUG)
  158. return
  159. # Bugs 1353135 & 1187508. Dbs can appear to be ready before the units
  160. # acl entry has been added. So, if the db supports passing a list of
  161. # permitted units then check if we're in the list.
  162. allowed_units = relation_get('nova_allowed_units', rid=db_rid, unit=unit)
  163. if skip_acl_check or (allowed_units and local_unit() in
  164. allowed_units.split()):
  165. status_set('maintenance', 'Running nova db migration')
  166. migrate_nova_databases()
  167. log('Triggering remote cloud-compute restarts.')
  168. [compute_joined(rid=rid, remote_restart=True)
  169. for rid in relation_ids('cloud-compute')]
  170. log('Triggering remote neutron-network-service restarts.')
  171. [quantum_joined(rid=rid, remote_restart=True)
  172. for rid in relation_ids('quantum-network-service')]
  173. if not skip_cells_restarts:
  174. log('Triggering remote cell restarts.')
  175. [nova_cell_relation_joined(rid=rid, remote_restart=True)
  176. for rid in relation_ids('cell')]
  177. else:
  178. log('allowed_units either not presented, or local unit '
  179. 'not in acl list: %s' % repr(allowed_units))
  180. def leader_init_db_if_ready_allowed_units():
  181. """Loop through all related db units and attempt to initialize db
  182. By looping through all related db units, the relation ID and unit can
  183. be passed to leader_init_db_if_ready(), enabling use of allowed_units
  184. to determine if this nova-cc unit is allowed to perform db init.
  185. """
  186. rels = ['shared-db', 'pgsql-nova-db']
  187. for rname in rels:
  188. for rid in relation_ids(rname):
  189. for unit in related_units(rid):
  190. if rname == 'pgsql-nova-db':
  191. leader_init_db_if_ready(skip_acl_check=True,
  192. skip_cells_restarts=True,
  193. db_rid=rid, unit=unit)
  194. else:
  195. leader_init_db_if_ready(db_rid=rid, unit=unit)
  196. def update_cell_db_if_ready(skip_acl_check=False, db_rid=None, unit=None):
  197. """Update the cells db if leader and db's are already intialised"""
  198. if not is_leader():
  199. return
  200. if not is_db_initialised():
  201. log("Database not initialised - skipping cell db update", level=DEBUG)
  202. return
  203. if not is_cellv2_init_ready():
  204. return
  205. allowed_units = relation_get('nova_allowed_units', rid=db_rid, unit=unit)
  206. if skip_acl_check or (allowed_units and local_unit() in
  207. allowed_units.split()):
  208. update_cell_database()
  209. else:
  210. log('allowed_units either not presented, or local unit '
  211. 'not in acl list: %s' % repr(allowed_units))
  212. def update_cell_db_if_ready_allowed_units():
  213. """Loop through all related db units and attempt to update cell db
  214. By looping through all related db units, the relation ID and unit can
  215. be passed to update_cell_db_if_ready(), enabling use of allowed_units
  216. to determine if this nova-cc unit is allowed to perform db updates.
  217. """
  218. rels = ['shared-db', 'pgsql-nova-db']
  219. for rname in rels:
  220. for rid in relation_ids(rname):
  221. for unit in related_units(rid):
  222. if rname == 'pgsql-nova-db':
  223. update_cell_db_if_ready(skip_acl_check=True,
  224. db_rid=rid, unit=unit)
  225. else:
  226. update_cell_db_if_ready(db_rid=rid, unit=unit)
  227. @hooks.hook('install.real')
  228. @harden()
  229. def install():
  230. status_set('maintenance', 'Executing pre-install')
  231. execd_preinstall()
  232. configure_installation_source(config('openstack-origin'))
  233. status_set('maintenance', 'Installing apt packages')
  234. apt_update()
  235. apt_install(determine_packages(), fatal=True)
  236. if placement_api_enabled():
  237. disable_package_apache_site()
  238. git_install(config('openstack-origin-git'))
  239. _files = os.path.join(charm_dir(), 'files')
  240. if os.path.isdir(_files):
  241. for f in os.listdir(_files):
  242. f = os.path.join(_files, f)
  243. if os.path.isfile(f):
  244. log('Installing %s to /usr/bin' % f)
  245. shutil.copy2(f, '/usr/bin')
  246. [open_port(port) for port in determine_ports()]
  247. msg = 'Disabling services into db relation joined'
  248. log(msg)
  249. status_set('maintenance', msg)
  250. disable_services()
  251. cmd_all_services('stop')
  252. @hooks.hook('config-changed')
  253. @service_guard(guard_map(), CONFIGS,
  254. active=config('service-guard'))
  255. @restart_on_change(restart_map(), stopstart=True)
  256. @harden()
  257. def config_changed():
  258. # neutron-server runs if < juno. Neutron-server creates mysql tables
  259. # which will subsequently cause db migratoins to fail if >= juno.
  260. # Disable neutron-server if >= juno
  261. if CompareOpenStackReleases(os_release('nova-common')) >= 'juno':
  262. with open('/etc/init/neutron-server.override', 'wb') as out:
  263. out.write('manual\n')
  264. if config('prefer-ipv6'):
  265. status_set('maintenance', 'configuring ipv6')
  266. setup_ipv6()
  267. sync_db_with_multi_ipv6_addresses(config('database'),
  268. config('database-user'),
  269. relation_prefix='nova')
  270. global CONFIGS
  271. if git_install_requested():
  272. status_set('maintenance', 'Running Git install')
  273. if config_value_changed('openstack-origin-git'):
  274. git_install(config('openstack-origin-git'))
  275. elif not config('action-managed-upgrade'):
  276. if openstack_upgrade_available('nova-common'):
  277. status_set('maintenance', 'Running openstack upgrade')
  278. do_openstack_upgrade(CONFIGS)
  279. [neutron_api_relation_joined(rid=rid, remote_restart=True)
  280. for rid in relation_ids('neutron-api')]
  281. # NOTE(jamespage): Force re-fire of shared-db joined hook
  282. # to ensure that nova_api database is setup if required.
  283. [db_joined(relation_id=r_id)
  284. for r_id in relation_ids('shared-db')]
  285. save_script_rc()
  286. configure_https()
  287. CONFIGS.write_all()
  288. # NOTE(jamespage): deal with any changes to the console and serial
  289. # console configuration options
  290. if not git_install_requested():
  291. filtered = filter_installed_packages(determine_packages())
  292. if filtered:
  293. apt_install(filtered, fatal=True)
  294. for rid in relation_ids('quantum-network-service'):
  295. quantum_joined(rid=rid)
  296. for r_id in relation_ids('identity-service'):
  297. identity_joined(rid=r_id)
  298. for rid in relation_ids('zeromq-configuration'):
  299. zeromq_configuration_relation_joined(rid)
  300. [cluster_joined(rid) for rid in relation_ids('cluster')]
  301. [compute_joined(rid=rid) for rid in relation_ids('cloud-compute')]
  302. update_nrpe_config()
  303. # If the region value has changed, notify the cloud-compute relations
  304. # to ensure the value is propagated to the compute nodes.
  305. if config_value_changed('region'):
  306. for rid in relation_ids('cloud-compute'):
  307. for unit in related_units(rid):
  308. compute_changed(rid, unit)
  309. update_nova_consoleauth_config()
  310. update_aws_compat_services()
  311. @hooks.hook('amqp-relation-joined')
  312. def amqp_joined(relation_id=None):
  313. relation_set(relation_id=relation_id,
  314. username=config('rabbit-user'), vhost=config('rabbit-vhost'))
  315. @hooks.hook('amqp-relation-changed')
  316. @hooks.hook('amqp-relation-departed')
  317. @service_guard(guard_map(), CONFIGS,
  318. active=config('service-guard'))
  319. @restart_on_change(restart_map())
  320. def amqp_changed():
  321. if 'amqp' not in CONFIGS.complete_contexts():
  322. log('amqp relation incomplete. Peer not ready?')
  323. return
  324. CONFIGS.write(NOVA_CONF)
  325. leader_init_db_if_ready_allowed_units()
  326. # db init for cells v2 requires amqp transport_url and db connections
  327. # to be set in nova.conf, so we attempt db init in here as well as the
  328. # db relation-changed hooks.
  329. update_cell_db_if_ready_allowed_units()
  330. [nova_cell_relation_joined(rid=rid)
  331. for rid in relation_ids('cell')]
  332. for r_id in relation_ids('nova-api'):
  333. nova_api_relation_joined(rid=r_id)
  334. # NOTE: trigger restart on nova-api-metadata on
  335. # neutron-gateway units once nova-cc has working
  336. # amqp connection (avoiding service down on n-gateway)
  337. for rid in relation_ids('quantum-network-service'):
  338. quantum_joined(rid=rid, remote_restart=True)
  339. @hooks.hook('shared-db-relation-joined')
  340. def db_joined(relation_id=None):
  341. if is_relation_made('pgsql-nova-db') or \
  342. is_relation_made('pgsql-neutron-db'):
  343. # error, postgresql is used
  344. e = ('Attempting to associate a mysql database when there is already '
  345. 'associated a postgresql one')
  346. log(e, level=ERROR)
  347. raise Exception(e)
  348. cmp_os_release = CompareOpenStackReleases(os_release('nova-common'))
  349. if config('prefer-ipv6'):
  350. sync_db_with_multi_ipv6_addresses(config('database'),
  351. config('database-user'),
  352. relation_prefix='nova')
  353. if cmp_os_release >= 'mitaka':
  354. # NOTE: mitaka uses a second nova-api database as well
  355. sync_db_with_multi_ipv6_addresses('nova_api',
  356. config('database-user'),
  357. relation_prefix='novaapi')
  358. if cmp_os_release >= 'ocata':
  359. # NOTE: ocata requires cells v2
  360. sync_db_with_multi_ipv6_addresses('nova_cell0',
  361. config('database-user'),
  362. relation_prefix='novacell0')
  363. else:
  364. # Avoid churn check for access-network early
  365. access_network = None
  366. for unit in related_units(relid=relation_id):
  367. access_network = relation_get(rid=relation_id, unit=unit,
  368. attribute='access-network')
  369. if access_network:
  370. break
  371. host = get_relation_ip('shared-db', cidr_network=access_network)
  372. relation_set(nova_database=config('database'),
  373. nova_username=config('database-user'),
  374. nova_hostname=host,
  375. relation_id=relation_id)
  376. if cmp_os_release >= 'mitaka':
  377. # NOTE: mitaka uses a second nova-api database as well
  378. relation_set(novaapi_database='nova_api',
  379. novaapi_username=config('database-user'),
  380. novaapi_hostname=host,
  381. relation_id=relation_id)
  382. if cmp_os_release >= 'ocata':
  383. # NOTE: ocata requires cells v2
  384. relation_set(novacell0_database='nova_cell0',
  385. novacell0_username=config('database-user'),
  386. novacell0_hostname=host,
  387. relation_id=relation_id)
  388. @hooks.hook('pgsql-nova-db-relation-joined')
  389. def pgsql_nova_db_joined():
  390. if is_relation_made('shared-db'):
  391. # raise error
  392. e = ('Attempting to associate a postgresql database'
  393. ' when there is already associated a mysql one')
  394. log(e, level=ERROR)
  395. raise Exception(e)
  396. relation_set(database=config('database'))
  397. @hooks.hook('shared-db-relation-changed')
  398. @service_guard(guard_map(), CONFIGS,
  399. active=config('service-guard'))
  400. @restart_on_change(restart_map())
  401. def db_changed():
  402. if 'shared-db' not in CONFIGS.complete_contexts():
  403. log('shared-db relation incomplete. Peer not ready?')
  404. return
  405. CONFIGS.write_all()
  406. leader_init_db_if_ready()
  407. # db init for cells v2 requires amqp transport_url and db connections to
  408. # be set in nova.conf, so we attempt db init in here as well as the
  409. # amqp-relation-changed hook.
  410. update_cell_db_if_ready()
  411. @hooks.hook('pgsql-nova-db-relation-changed')
  412. @service_guard(guard_map(), CONFIGS,
  413. active=config('service-guard'))
  414. @restart_on_change(restart_map())
  415. def postgresql_nova_db_changed():
  416. if 'pgsql-nova-db' not in CONFIGS.complete_contexts():
  417. log('pgsql-nova-db relation incomplete. Peer not ready?')
  418. return
  419. CONFIGS.write_all()
  420. leader_init_db_if_ready(skip_acl_check=True, skip_cells_restarts=True)
  421. update_cell_db_if_ready(skip_acl_check=True)
  422. for r_id in relation_ids('nova-api'):
  423. nova_api_relation_joined(rid=r_id)
  424. @hooks.hook('image-service-relation-changed')
  425. @service_guard(guard_map(), CONFIGS,
  426. active=config('service-guard'))
  427. @restart_on_change(restart_map())
  428. def image_service_changed():
  429. if 'image-service' not in CONFIGS.complete_contexts():
  430. log('image-service relation incomplete. Peer not ready?')
  431. return
  432. CONFIGS.write(NOVA_CONF)
  433. # TODO: special case config flag for essex (strip protocol)
  434. for r_id in relation_ids('nova-api'):
  435. nova_api_relation_joined(rid=r_id)
  436. @hooks.hook('identity-service-relation-joined')
  437. def identity_joined(rid=None):
  438. if config('vip') and not is_clustered():
  439. log('Defering registration until clustered', level=DEBUG)
  440. return
  441. public_url = canonical_url(CONFIGS, PUBLIC)
  442. internal_url = canonical_url(CONFIGS, INTERNAL)
  443. admin_url = canonical_url(CONFIGS, ADMIN)
  444. relation_set(
  445. relation_id=rid,
  446. **determine_endpoints(public_url,
  447. internal_url,
  448. admin_url))
  449. @hooks.hook('identity-service-relation-changed')
  450. @service_guard(guard_map(), CONFIGS,
  451. active=config('service-guard'))
  452. @restart_on_change(restart_map())
  453. def identity_changed():
  454. if 'identity-service' not in CONFIGS.complete_contexts():
  455. log('identity-service relation incomplete. Peer not ready?')
  456. return
  457. CONFIGS.write('/etc/nova/api-paste.ini')
  458. CONFIGS.write(NOVA_CONF)
  459. [compute_joined(rid) for rid in relation_ids('cloud-compute')]
  460. [quantum_joined(rid) for rid in relation_ids('quantum-network-service')]
  461. [nova_vmware_relation_joined(rid) for rid in relation_ids('nova-vmware')]
  462. [neutron_api_relation_joined(rid) for rid in relation_ids('neutron-api')]
  463. configure_https()
  464. for r_id in relation_ids('nova-api'):
  465. nova_api_relation_joined(rid=r_id)
  466. @hooks.hook('nova-volume-service-relation-joined',
  467. 'cinder-volume-service-relation-joined')
  468. @service_guard(guard_map(), CONFIGS,
  469. active=config('service-guard'))
  470. @restart_on_change(restart_map())
  471. def volume_joined():
  472. CONFIGS.write(NOVA_CONF)
  473. # kick identity_joined() to publish possibly new nova-volume endpoint.
  474. [identity_joined(rid) for rid in relation_ids('identity-service')]
  475. def _auth_config():
  476. '''Grab all KS auth token config from api-paste.ini, or return empty {}'''
  477. ks_auth_host = auth_token_config('auth_host')
  478. if not ks_auth_host:
  479. # if there is no auth_host set, identity-service changed hooks
  480. # have not fired, yet.
  481. return {}
  482. cfg = {
  483. 'auth_host': ks_auth_host,
  484. 'auth_port': auth_token_config('auth_port'),
  485. 'auth_protocol': auth_token_config('auth_protocol'),
  486. 'service_protocol': auth_token_config('service_protocol'),
  487. 'service_port': auth_token_config('service_port'),
  488. 'service_username': auth_token_config('admin_user'),
  489. 'service_password': auth_token_config('admin_password'),
  490. 'service_tenant_name': auth_token_config('admin_tenant_name'),
  491. 'auth_uri': auth_token_config('auth_uri'),
  492. # quantum-gateway interface deviates a bit.
  493. 'keystone_host': ks_auth_host,
  494. 'service_tenant': auth_token_config('admin_tenant_name'),
  495. # add api version if found
  496. 'api_version': auth_token_config('api_version') or '2.0',
  497. }
  498. return cfg
  499. def save_novarc():
  500. auth = _auth_config()
  501. # XXX hard-coded http
  502. ks_url = '%s://%s:%s/v%s' % (auth['auth_protocol'],
  503. auth['auth_host'],
  504. auth['auth_port'],
  505. auth['api_version'])
  506. with open('/etc/quantum/novarc', 'wb') as out:
  507. out.write('export OS_USERNAME=%s\n' % auth['service_username'])
  508. out.write('export OS_PASSWORD=%s\n' % auth['service_password'])
  509. out.write('export OS_TENANT_NAME=%s\n' % auth['service_tenant_name'])
  510. out.write('export OS_AUTH_URL=%s\n' % ks_url)
  511. out.write('export OS_REGION_NAME=%s\n' % config('region'))
  512. def neutron_settings():
  513. neutron_settings = {}
  514. if is_relation_made('neutron-api', 'neutron-plugin'):
  515. neutron_api_info = NeutronAPIContext()()
  516. neutron_settings.update({
  517. # XXX: Rename these relations settings?
  518. 'quantum_plugin': neutron_api_info['neutron_plugin'],
  519. 'region': config('region'),
  520. 'quantum_security_groups':
  521. neutron_api_info['neutron_security_groups'],
  522. 'quantum_url': neutron_api_info['neutron_url'],
  523. })
  524. neutron_url = urlparse(neutron_settings['quantum_url'])
  525. neutron_settings['quantum_host'] = neutron_url.hostname
  526. neutron_settings['quantum_port'] = neutron_url.port
  527. return neutron_settings
  528. def keystone_compute_settings():
  529. ks_auth_config = _auth_config()
  530. rel_settings = {}
  531. if network_manager() == 'neutron':
  532. if ks_auth_config:
  533. rel_settings.update(ks_auth_config)
  534. rel_settings.update(neutron_settings())
  535. ks_ca = keystone_ca_cert_b64()
  536. if ks_auth_config and ks_ca:
  537. rel_settings['ca_cert'] = ks_ca
  538. return rel_settings
  539. def console_settings():
  540. rel_settings = {}
  541. proto = console_attributes('protocol')
  542. if not proto:
  543. return {}
  544. rel_settings['console_keymap'] = config('console-keymap')
  545. rel_settings['console_access_protocol'] = proto
  546. console_ssl = False
  547. if config('console-ssl-cert') and config('console-ssl-key'):
  548. console_ssl = True
  549. if config('console-proxy-ip') == 'local':
  550. if console_ssl:
  551. address = resolve_address(endpoint_type=PUBLIC)
  552. address = format_ipv6_addr(address) or address
  553. proxy_base_addr = 'https://%s' % address
  554. else:
  555. # canonical_url will only return 'https:' if API SSL are enabled.
  556. proxy_base_addr = canonical_url(CONFIGS, PUBLIC)
  557. else:
  558. if console_ssl or https():
  559. schema = "https"
  560. else:
  561. schema = "http"
  562. proxy_base_addr = "%s://%s" % (schema, config('console-proxy-ip'))
  563. if proto == 'vnc':
  564. protocols = ['novnc', 'xvpvnc']
  565. else:
  566. protocols = [proto]
  567. for _proto in protocols:
  568. rel_settings['console_proxy_%s_address' % (_proto)] = \
  569. "%s:%s%s" % (proxy_base_addr,
  570. console_attributes('proxy-port', proto=_proto),
  571. console_attributes('proxy-page', proto=_proto))
  572. rel_settings['console_proxy_%s_host' % (_proto)] = \
  573. urlparse(proxy_base_addr).hostname
  574. rel_settings['console_proxy_%s_port' % (_proto)] = \
  575. console_attributes('proxy-port', proto=_proto)
  576. return rel_settings
  577. @hooks.hook('cloud-compute-relation-joined')
  578. def compute_joined(rid=None, remote_restart=False):
  579. cons_settings = console_settings()
  580. relation_set(relation_id=rid, **cons_settings)
  581. rel_settings = {
  582. 'network_manager': network_manager(),
  583. 'volume_service': 'cinder',
  584. # (comment from bash vers) XXX Should point to VIP if clustered, or
  585. # this may not even be needed.
  586. 'ec2_host': unit_get('private-address'),
  587. 'region': config('region'),
  588. }
  589. rel_settings.update(serial_console_settings())
  590. # update relation setting if we're attempting to restart remote
  591. # services
  592. if remote_restart:
  593. rel_settings['restart_trigger'] = str(uuid.uuid4())
  594. rel_settings.update(keystone_compute_settings())
  595. relation_set(relation_id=rid, **rel_settings)
  596. @hooks.hook('cloud-compute-relation-changed')
  597. def compute_changed(rid=None, unit=None):
  598. for r_id in relation_ids('nova-api'):
  599. nova_api_relation_joined(rid=r_id)
  600. rel_settings = relation_get(rid=rid, unit=unit)
  601. if not rel_settings.get('region', None) == config('region'):
  602. relation_set(relation_id=rid, region=config('region'))
  603. if is_cellv2_init_ready() and is_db_initialised():
  604. add_hosts_to_cell()
  605. if 'migration_auth_type' not in rel_settings:
  606. return
  607. if rel_settings['migration_auth_type'] == 'ssh':
  608. status_set('maintenance', 'configuring live migration')
  609. key = rel_settings.get('ssh_public_key')
  610. if not key:
  611. log('SSH migration set but peer did not publish key.')
  612. return
  613. ssh_compute_add(key, rid=rid, unit=unit)
  614. index = 0
  615. for line in ssh_known_hosts_lines(unit=unit):
  616. relation_set(
  617. relation_id=rid,
  618. relation_settings={
  619. 'known_hosts_{}'.format(index): line})
  620. index += 1
  621. relation_set(relation_id=rid, known_hosts_max_index=index)
  622. index = 0
  623. for line in ssh_authorized_keys_lines(unit=unit):
  624. relation_set(
  625. relation_id=rid,
  626. relation_settings={
  627. 'authorized_keys_{}'.format(index): line})
  628. index += 1
  629. relation_set(relation_id=rid, authorized_keys_max_index=index)
  630. if 'nova_ssh_public_key' not in rel_settings:
  631. return
  632. if rel_settings['nova_ssh_public_key']:
  633. ssh_compute_add(rel_settings['nova_ssh_public_key'],
  634. rid=rid, unit=unit, user='nova')
  635. index = 0
  636. for line in ssh_known_hosts_lines(unit=unit, user='nova'):
  637. relation_set(
  638. relation_id=rid,
  639. relation_settings={
  640. '{}_known_hosts_{}'.format(
  641. 'nova',
  642. index): line})
  643. index += 1
  644. relation_set(
  645. relation_id=rid,
  646. relation_settings={
  647. '{}_known_hosts_max_index'.format('nova'): index})
  648. index = 0
  649. for line in ssh_authorized_keys_lines(unit=unit, user='nova'):
  650. relation_set(
  651. relation_id=rid,
  652. relation_settings={
  653. '{}_authorized_keys_{}'.format(
  654. 'nova',
  655. index): line})
  656. index += 1
  657. relation_set(
  658. relation_id=rid,
  659. relation_settings={
  660. '{}_authorized_keys_max_index'.format('nova'): index})
  661. @hooks.hook('cloud-compute-relation-departed')
  662. def compute_departed():
  663. ssh_compute_remove(public_key=relation_get('ssh_public_key'))
  664. @hooks.hook('neutron-network-service-relation-joined',
  665. 'quantum-network-service-relation-joined')
  666. def quantum_joined(rid=None, remote_restart=False):
  667. rel_settings = neutron_settings()
  668. # inform quantum about local keystone auth config
  669. ks_auth_config = _auth_config()
  670. rel_settings.update(ks_auth_config)
  671. # must pass the keystone CA cert, if it exists.
  672. ks_ca = keystone_ca_cert_b64()
  673. if ks_auth_config and ks_ca:
  674. rel_settings['ca_cert'] = ks_ca
  675. # update relation setting if we're attempting to restart remote
  676. # services
  677. if remote_restart:
  678. rel_settings['restart_trigger'] = str(uuid.uuid4())
  679. relation_set(relation_id=rid, **rel_settings)
  680. @hooks.hook('cluster-relation-joined')
  681. def cluster_joined(relation_id=None):
  682. settings = {}
  683. for addr_type in ADDRESS_TYPES:
  684. address = get_relation_ip(
  685. addr_type,
  686. cidr_network=config('os-{}-network'.format(addr_type)))
  687. if address:
  688. settings['{}-address'.format(addr_type)] = address
  689. settings['private-address'] = get_relation_ip('cluster')
  690. relation_set(relation_id=relation_id, relation_settings=settings)
  691. @hooks.hook('cluster-relation-changed',
  692. 'cluster-relation-departed',
  693. 'leader-settings-changed')
  694. @service_guard(guard_map(), CONFIGS,
  695. active=config('service-guard'))
  696. @restart_on_change(restart_map(), stopstart=True)
  697. def cluster_changed():
  698. CONFIGS.write_all()
  699. if relation_ids('cluster'):
  700. peer_echo(includes=['dbsync_state'])
  701. dbsync_state = peer_retrieve('dbsync_state')
  702. if dbsync_state == 'complete':
  703. enable_services()
  704. cmd_all_services('start')
  705. else:
  706. log('Database sync not ready. Shutting down services')
  707. disable_services()
  708. cmd_all_services('stop')
  709. @hooks.hook('ha-relation-joined')
  710. def ha_joined(relation_id=None):
  711. cluster_config = get_hacluster_config()
  712. resources = {
  713. 'res_nova_haproxy': 'lsb:haproxy',
  714. }
  715. resource_params = {
  716. 'res_nova_haproxy': 'op monitor interval="5s"',
  717. }
  718. init_services = {
  719. 'res_nova_haproxy': 'haproxy'
  720. }
  721. clones = {
  722. 'cl_nova_haproxy': 'res_nova_haproxy'
  723. }
  724. colocations = {}
  725. if config('dns-ha'):
  726. update_dns_ha_resource_params(relation_id=relation_id,
  727. resources=resources,
  728. resource_params=resource_params)
  729. else:
  730. vip_group = []
  731. for vip in cluster_config['vip'].split():
  732. if is_ipv6(vip):
  733. res_nova_vip = 'ocf:heartbeat:IPv6addr'
  734. vip_params = 'ipv6addr'
  735. else:
  736. res_nova_vip = 'ocf:heartbeat:IPaddr2'
  737. vip_params = 'ip'
  738. iface = (get_iface_for_address(vip) or
  739. config('vip_iface'))
  740. netmask = (get_netmask_for_address(vip) or
  741. config('vip_cidr'))
  742. if iface is not None:
  743. vip_key = 'res_nova_{}_vip'.format(iface)
  744. if vip_key in vip_group:
  745. if vip not in resource_params[vip_key]:
  746. vip_key = '{}_{}'.format(vip_key, vip_params)
  747. else:
  748. log("Resource '%s' (vip='%s') already exists in "
  749. "vip group - skipping" % (vip_key, vip), WARNING)
  750. continue
  751. resources[vip_key] = res_nova_vip
  752. resource_params[vip_key] = (
  753. 'params {ip}="{vip}" cidr_netmask="{netmask}"'
  754. ' nic="{iface}"'.format(ip=vip_params,
  755. vip=vip,
  756. iface=iface,
  757. netmask=netmask)
  758. )
  759. vip_group.append(vip_key)
  760. if len(vip_group) >= 1:
  761. relation_set(groups={'grp_nova_vips': ' '.join(vip_group)})
  762. if (config('single-nova-consoleauth') and
  763. console_attributes('protocol')):
  764. colocations['vip_consoleauth'] = COLO_CONSOLEAUTH
  765. init_services['res_nova_consoleauth'] = 'nova-consoleauth'
  766. resources['res_nova_consoleauth'] = AGENT_CONSOLEAUTH
  767. resource_params['res_nova_consoleauth'] = AGENT_CA_PARAMS
  768. relation_set(relation_id=relation_id,
  769. init_services=init_services,
  770. corosync_bindiface=cluster_config['ha-bindiface'],
  771. corosync_mcastport=cluster_config['ha-mcastport'],
  772. resources=resources,
  773. resource_params=resource_params,
  774. clones=clones,
  775. colocations=colocations)
  776. @hooks.hook('ha-relation-changed')
  777. def ha_changed():
  778. clustered = relation_get('clustered')
  779. if not clustered or clustered in [None, 'None', '']:
  780. log('ha_changed: hacluster subordinate not fully clustered.')
  781. return
  782. CONFIGS.write(NOVA_CONF)
  783. log('Cluster configured, notifying other services and updating '
  784. 'keystone endpoint configuration')
  785. for rid in relation_ids('identity-service'):
  786. identity_joined(rid=rid)
  787. update_nova_consoleauth_config()
  788. @hooks.hook('shared-db-relation-broken',
  789. 'pgsql-nova-db-relation-broken')
  790. @service_guard(guard_map(), CONFIGS,
  791. active=config('service-guard'))
  792. def db_departed():
  793. CONFIGS.write_all()
  794. update_cell_db_if_ready(skip_acl_check=True)
  795. for r_id in relation_ids('cluster'):
  796. relation_set(relation_id=r_id, dbsync_state='incomplete')
  797. disable_services()
  798. cmd_all_services('stop')
  799. @hooks.hook('amqp-relation-broken',
  800. 'cinder-volume-service-relation-broken',
  801. 'identity-service-relation-broken',
  802. 'image-service-relation-broken',
  803. 'nova-volume-service-relation-broken',
  804. 'pgsql-neutron-db-relation-broken',
  805. 'quantum-network-service-relation-broken')
  806. @service_guard(guard_map(), CONFIGS,
  807. active=config('service-guard'))
  808. def relation_broken():
  809. CONFIGS.write_all()
  810. [nova_cell_relation_joined(rid=rid)
  811. for rid in relation_ids('cell')]
  812. def configure_https():
  813. '''
  814. Enables SSL API Apache config if appropriate and kicks identity-service
  815. with any required api updates.
  816. '''
  817. # need to write all to ensure changes to the entire request pipeline
  818. # propagate (c-api, haprxy, apache)
  819. CONFIGS.write_all()
  820. if 'https' in CONFIGS.complete_contexts():
  821. cmd = ['a2ensite', 'openstack_https_frontend']
  822. check_call(cmd)
  823. else:
  824. cmd = ['a2dissite', 'openstack_https_frontend']
  825. check_call(cmd)
  826. # TODO: improve this by checking if local CN certs are available
  827. # first then checking reload status (see LP #1433114).
  828. if not is_unit_paused_set():
  829. service_reload('apache2', restart_on_failure=True)
  830. for rid in relation_ids('identity-service'):
  831. identity_joined(rid=rid)
  832. @hooks.hook()
  833. def nova_vmware_relation_joined(rid=None):
  834. rel_settings = {'network_manager': network_manager()}
  835. ks_auth = _auth_config()
  836. if ks_auth:
  837. rel_settings.update(ks_auth)
  838. rel_settings.update(neutron_settings())
  839. relation_set(relation_id=rid, **rel_settings)
  840. @hooks.hook('nova-vmware-relation-changed')
  841. @service_guard(guard_map(), CONFIGS,
  842. active=config('service-guard'))
  843. @restart_on_change(restart_map())
  844. def nova_vmware_relation_changed():
  845. CONFIGS.write('/etc/nova/nova.conf')
  846. @hooks.hook('upgrade-charm')
  847. @harden()
  848. def upgrade_charm():
  849. apt_install(filter_installed_packages(determine_packages()),
  850. fatal=True)
  851. for r_id in relation_ids('amqp'):
  852. amqp_joined(relation_id=r_id)
  853. for r_id in relation_ids('identity-service'):
  854. identity_joined(rid=r_id)
  855. for r_id in relation_ids('cloud-compute'):
  856. for unit in related_units(r_id):
  857. compute_changed(r_id, unit)
  858. for r_id in relation_ids('shared-db'):
  859. db_joined(relation_id=r_id)
  860. leader_init_db_if_ready_allowed_units()
  861. update_nrpe_config()
  862. update_nova_consoleauth_config()
  863. # remote_restart is defaulted to true as nova-cells may have started the
  864. # nova-cell process before the db migration was run so it will need a
  865. # kick
  866. @hooks.hook('cell-relation-joined')
  867. def nova_cell_relation_joined(rid=None, remote_restart=True):
  868. rel_settings = {
  869. 'nova_url': "%s:8774/v2" % canonical_url(CONFIGS, INTERNAL)
  870. }
  871. if remote_restart:
  872. rel_settings['restart_trigger'] = str(uuid.uuid4())
  873. relation_set(relation_id=rid, **rel_settings)
  874. @hooks.hook('cell-relation-changed')
  875. @restart_on_change(restart_map())
  876. def nova_cell_relation_changed():
  877. CONFIGS.write(NOVA_CONF)
  878. def get_cell_type():
  879. cell_info = NovaCellContext()()
  880. if 'cell_type' in cell_info:
  881. return cell_info['cell_type']
  882. return None
  883. @hooks.hook('neutron-api-relation-joined')
  884. def neutron_api_relation_joined(rid=None, remote_restart=False):
  885. for id_rid in relation_ids('identity-service'):
  886. identity_joined(rid=id_rid)
  887. rel_settings = {
  888. 'nova_url': canonical_url(CONFIGS, INTERNAL) + ":8774/v2"
  889. }
  890. if get_cell_type():
  891. rel_settings['cell_type'] = get_cell_type()
  892. if remote_restart:
  893. rel_settings['restart_trigger'] = str(uuid.uuid4())
  894. relation_set(relation_id=rid, **rel_settings)
  895. @hooks.hook('neutron-api-relation-changed')
  896. @service_guard(guard_map(), CONFIGS,
  897. active=config('service-guard'))
  898. @restart_on_change(restart_map())
  899. def neutron_api_relation_changed():
  900. CONFIGS.write(NOVA_CONF)
  901. for rid in relation_ids('cloud-compute'):
  902. compute_joined(rid=rid)
  903. for rid in relation_ids('quantum-network-service'):
  904. quantum_joined(rid=rid)
  905. @hooks.hook('neutron-api-relation-broken')
  906. @service_guard(guard_map(), CONFIGS,
  907. active=config('service-guard'))
  908. @restart_on_change(restart_map())
  909. def neutron_api_relation_broken():
  910. CONFIGS.write_all()
  911. for rid in relation_ids('cloud-compute'):
  912. compute_joined(rid=rid)
  913. for rid in relation_ids('quantum-network-service'):
  914. quantum_joined(rid=rid)
  915. @hooks.hook('zeromq-configuration-relation-joined')
  916. @os_requires_version('kilo', 'nova-common')
  917. def zeromq_configuration_relation_joined(relid=None):
  918. relation_set(relation_id=relid,
  919. topics=" ".join(get_topics()),
  920. users="nova")
  921. @hooks.hook('nrpe-external-master-relation-joined',
  922. 'nrpe-external-master-relation-changed')
  923. def update_nrpe_config():
  924. # python-dbus is used by check_upstart_job
  925. apt_install('python-dbus')
  926. hostname = nrpe.get_nagios_hostname()
  927. current_unit = nrpe.get_nagios_unit_name()
  928. nrpe_setup = nrpe.NRPE(hostname=hostname)
  929. nrpe.copy_nrpe_checks()
  930. nrpe.add_init_service_checks(nrpe_setup, services(), current_unit)
  931. nrpe.add_haproxy_checks(nrpe_setup, current_unit)
  932. nrpe_setup.write()
  933. @hooks.hook('memcache-relation-joined')
  934. def memcached_joined():
  935. """When memcache relation joins we want to set our private address as the
  936. spaces address rather than leaving it as the unit address. This is to
  937. support network spaces in the memcached charm.
  938. """
  939. relation_set(
  940. relation_id=None,
  941. relation_settings={'private-address': get_relation_ip('memcache')})
  942. memcached_common()
  943. @hooks.hook('memcache-relation-departed',
  944. 'memcache-relation-changed',
  945. 'memcache-relation-broken')
  946. def memcached_other_hooks():
  947. memcached_common()
  948. @restart_on_change(restart_map())
  949. def memcached_common():
  950. CONFIGS.write(NOVA_CONF)
  951. @hooks.hook('zeromq-configuration-relation-changed')
  952. @restart_on_change(restart_map(), stopstart=True)
  953. def zeromq_configuration_relation_changed():
  954. CONFIGS.write(NOVA_CONF)
  955. def update_nova_consoleauth_config():
  956. """
  957. Configure nova-consoleauth pacemaker resources
  958. """
  959. relids = relation_ids('ha')
  960. if len(relids) == 0:
  961. log('Related to {} ha services'.format(len(relids)), level='DEBUG')
  962. ha_relid = None
  963. data = {}
  964. else:
  965. ha_relid = relids[0]
  966. data = relation_get(rid=ha_relid) or {}
  967. # initialize keys in case this is a new dict
  968. data.setdefault('delete_resources', [])
  969. for k in ['colocations', 'init_services', 'resources', 'resource_params']:
  970. data.setdefault(k, {})
  971. if config('single-nova-consoleauth') and console_attributes('protocol'):
  972. for item in ['vip_consoleauth', 'res_nova_consoleauth']:
  973. try:
  974. data['delete_resources'].remove(item)
  975. except ValueError:
  976. pass # nothing to remove, we are good
  977. # the new pcmkr resources have to be added to the existing ones
  978. data['colocations']['vip_consoleauth'] = COLO_CONSOLEAUTH
  979. data['init_services']['res_nova_consoleauth'] = 'nova-consoleauth'
  980. data['resources']['res_nova_consoleauth'] = AGENT_CONSOLEAUTH
  981. data['resource_params']['res_nova_consoleauth'] = AGENT_CA_PARAMS
  982. for rid in relation_ids('ha'):
  983. relation_set(rid, **data)
  984. # nova-consoleauth will be managed by pacemaker, so stop it
  985. # and prevent it to be started again at boot. (LP: #1693629).
  986. if relation_ids('ha'):
  987. service_pause('nova-consoleauth')
  988. elif (not config('single-nova-consoleauth') and
  989. console_attributes('protocol')):
  990. for item in ['vip_consoleauth', 'res_nova_consoleauth']:
  991. if item not in data['delete_resources']:
  992. data['delete_resources'].append(item)
  993. # remove them from the rel, so they aren't recreated when the hook
  994. # is recreated
  995. data['colocations'].pop('vip_consoleauth', None)
  996. data['init_services'].pop('res_nova_consoleauth', None)
  997. data['resources'].pop('res_nova_consoleauth', None)
  998. data['resource_params'].pop('res_nova_consoleauth', None)
  999. for rid in relation_ids('ha'):
  1000. relation_set(rid, **data)
  1001. try:
  1002. os.remove(NOVA_CONSOLEAUTH_OVERRIDE)
  1003. except FileNotFoundError as e:
  1004. log(str(e), level='DEBUG')
  1005. def nova_api_relation_joined(rid=None):
  1006. rel_data = {
  1007. 'nova-api-ready': 'yes' if is_api_ready(CONFIGS) else 'no'
  1008. }
  1009. relation_set(rid, **rel_data)
  1010. @hooks.hook('update-status')
  1011. @harden()
  1012. def update_status():
  1013. log('Updating status.')
  1014. def main():
  1015. try:
  1016. hooks.execute(sys.argv)
  1017. except UnregisteredHookError as e:
  1018. log('Unknown hook {} - skipping.'.format(e))
  1019. assess_status(CONFIGS)
  1020. if __name__ == '__main__':
  1021. main()