A utility to run diskimage-builder undercloud elements on a running host
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.

undercloud.py 63KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605
  1. # Copyright 2015 Red Hat Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. from __future__ import print_function
  15. import copy
  16. import errno
  17. import getpass
  18. import glob
  19. import hashlib
  20. import json
  21. import logging
  22. import os
  23. import platform
  24. import socket
  25. import subprocess
  26. import sys
  27. import tempfile
  28. import time
  29. import uuid
  30. import yaml
  31. from keystoneauth1 import session
  32. from keystoneauth1 import exceptions as ks_exceptions
  33. from keystoneclient import discover
  34. import keystoneauth1.identity.generic as ks_auth
  35. from mistralclient.api import client as mistralclient
  36. import novaclient as nc
  37. from novaclient import client as novaclient
  38. from novaclient import exceptions
  39. from oslo_config import cfg
  40. import psutil
  41. import pystache
  42. import six
  43. from swiftclient import client as swiftclient
  44. from instack_undercloud import validator
  45. # Making these values properties on a class allows us to delay their lookup,
  46. # which makes testing code that interacts with these files much easier.
  47. # NOTE(bnemec): The unit tests rely on these paths being in ~. If they are
  48. # ever moved the tests may need to be updated to avoid overwriting real files.
  49. class Paths(object):
  50. @property
  51. def CONF_PATH(self):
  52. return os.path.expanduser('~/undercloud.conf')
  53. # NOTE(bnemec): Deprecated
  54. @property
  55. def ANSWERS_PATH(self):
  56. return os.path.expanduser('~/instack.answers')
  57. @property
  58. def PASSWORD_PATH(self):
  59. return os.path.expanduser('~/undercloud-passwords.conf')
  60. @property
  61. def LOG_FILE(self):
  62. return os.path.expanduser('~/.instack/install-undercloud.log')
  63. PATHS = Paths()
  64. DEFAULT_LOG_LEVEL = logging.DEBUG
  65. DEFAULT_LOG_FORMAT = '%(asctime)s %(levelname)s: %(message)s'
  66. LOG = None
  67. CONF = cfg.CONF
  68. COMPLETION_MESSAGE = """
  69. #############################################################################
  70. Undercloud %(undercloud_operation)s complete.
  71. The file containing this installation's passwords is at
  72. %(password_path)s.
  73. There is also a stackrc file at %(stackrc_path)s.
  74. These files are needed to interact with the OpenStack services, and should be
  75. secured.
  76. #############################################################################
  77. """
  78. FAILURE_MESSAGE = """
  79. #############################################################################
  80. Undercloud %(undercloud_operation)s failed.
  81. Reason: %(exception)s
  82. See the previous output for details about what went wrong. The full install
  83. log can be found at %(log_file)s.
  84. #############################################################################
  85. """
  86. # We need 8 GB, leave a little room for variation in what 8 GB means on
  87. # different platforms.
  88. REQUIRED_MB = 7680
  89. # When adding new options to the lists below, make sure to regenerate the
  90. # sample config by running "tox -e genconfig" in the project root.
  91. _opts = [
  92. cfg.StrOpt('undercloud_hostname',
  93. help=('Fully qualified hostname (including domain) to set on '
  94. 'the Undercloud. If left unset, the '
  95. 'current hostname will be used, but the user is '
  96. 'responsible for configuring all system hostname '
  97. 'settings appropriately. If set, the undercloud install '
  98. 'will configure all system hostname settings.'),
  99. ),
  100. cfg.StrOpt('local_ip',
  101. default='192.168.24.1/24',
  102. help=('IP information for the interface on the Undercloud '
  103. 'that will be handling the PXE boots and DHCP for '
  104. 'Overcloud instances. The IP portion of the value will '
  105. 'be assigned to the network interface defined by '
  106. 'local_interface, with the netmask defined by the '
  107. 'prefix portion of the value.')
  108. ),
  109. cfg.StrOpt('network_gateway',
  110. default='192.168.24.1',
  111. help=('Network gateway for the Neutron-managed network for '
  112. 'Overcloud instances. This should match the local_ip '
  113. 'above when using masquerading.')
  114. ),
  115. cfg.StrOpt('undercloud_public_host',
  116. deprecated_name='undercloud_public_vip',
  117. default='192.168.24.2',
  118. help=('Virtual IP or DNS address to use for the public '
  119. 'endpoints of Undercloud services. Only used with SSL.')
  120. ),
  121. cfg.StrOpt('undercloud_admin_host',
  122. deprecated_name='undercloud_admin_vip',
  123. default='192.168.24.3',
  124. help=('Virtual IP or DNS address to use for the admin '
  125. 'endpoints of Undercloud services. Only used with SSL.')
  126. ),
  127. cfg.ListOpt('undercloud_nameservers',
  128. default=[],
  129. help=('DNS nameserver(s) to use for the undercloud node.'),
  130. ),
  131. cfg.ListOpt('undercloud_ntp_servers',
  132. default=[],
  133. help=('List of ntp servers to use.')),
  134. cfg.StrOpt('overcloud_domain_name',
  135. default='localdomain',
  136. help=('DNS domain name to use when deploying the overcloud. '
  137. 'The overcloud parameter "CloudDomain" must be set to a '
  138. 'matching value.')
  139. ),
  140. cfg.StrOpt('undercloud_service_certificate',
  141. default='',
  142. help=('Certificate file to use for OpenStack service SSL '
  143. 'connections. Setting this enables SSL for the '
  144. 'OpenStack API endpoints, leaving it unset disables SSL.')
  145. ),
  146. cfg.BoolOpt('generate_service_certificate',
  147. default=False,
  148. help=('When set to True, an SSL certificate will be generated '
  149. 'as part of the undercloud install and this certificate '
  150. 'will be used in place of the value for '
  151. 'undercloud_service_certificate. The resulting '
  152. 'certificate will be written to '
  153. '/etc/pki/tls/certs/undercloud-[undercloud_public_host].'
  154. 'pem. This certificate is signed by CA selected by the '
  155. '"certificate_generation_ca" option.')
  156. ),
  157. cfg.StrOpt('certificate_generation_ca',
  158. default='local',
  159. help=('The certmonger nickname of the CA from which the '
  160. 'certificate will be requested. This is used only if '
  161. 'the generate_service_certificate option is set. '
  162. 'Note that if the "local" CA is selected the '
  163. 'certmonger\'s local CA certificate will be extracted to '
  164. '/etc/pki/ca-trust/source/anchors/cm-local-ca.pem and '
  165. 'subsequently added to the trust chain.')
  166. ),
  167. cfg.StrOpt('service_principal',
  168. default='',
  169. help=('The kerberos principal for the service that will use '
  170. 'the certificate. This is only needed if your CA '
  171. 'requires a kerberos principal. e.g. with FreeIPA.')
  172. ),
  173. cfg.StrOpt('local_interface',
  174. default='eth1',
  175. help=('Network interface on the Undercloud that will be '
  176. 'handling the PXE boots and DHCP for Overcloud '
  177. 'instances.')
  178. ),
  179. cfg.IntOpt('local_mtu',
  180. default=1500,
  181. help=('MTU to use for the local_interface.')
  182. ),
  183. cfg.StrOpt('network_cidr',
  184. default='192.168.24.0/24',
  185. help=('Network CIDR for the Neutron-managed network for '
  186. 'Overcloud instances. This should be the subnet used '
  187. 'for PXE booting.')
  188. ),
  189. cfg.StrOpt('masquerade_network',
  190. default='192.168.24.0/24',
  191. help=('Network that will be masqueraded for external access, '
  192. 'if required. This should be the subnet used for PXE '
  193. 'booting.')
  194. ),
  195. cfg.StrOpt('dhcp_start',
  196. default='192.168.24.5',
  197. help=('Start of DHCP allocation range for PXE and DHCP of '
  198. 'Overcloud instances.')
  199. ),
  200. cfg.StrOpt('dhcp_end',
  201. default='192.168.24.24',
  202. help=('End of DHCP allocation range for PXE and DHCP of '
  203. 'Overcloud instances.')
  204. ),
  205. cfg.StrOpt('hieradata_override',
  206. default='',
  207. help=('Path to hieradata override file. If set, the file will '
  208. 'be copied under /etc/puppet/hieradata and set as the '
  209. 'first file in the hiera hierarchy. This can be used '
  210. 'to custom configure services beyond what '
  211. 'undercloud.conf provides')
  212. ),
  213. cfg.StrOpt('net_config_override',
  214. default='',
  215. help=('Path to network config override template. If set, this '
  216. 'template will be used to configure the networking via '
  217. 'os-net-config. Must be in json format. '
  218. 'Templated tags can be used within the '
  219. 'template, see '
  220. 'instack-undercloud/elements/undercloud-stack-config/'
  221. 'net-config.json.template for example tags')
  222. ),
  223. cfg.StrOpt('inspection_interface',
  224. default='br-ctlplane',
  225. deprecated_name='discovery_interface',
  226. help=('Network interface on which inspection dnsmasq will '
  227. 'listen. If in doubt, use the default value.')
  228. ),
  229. cfg.StrOpt('inspection_iprange',
  230. default='192.168.24.100,192.168.24.120',
  231. deprecated_name='discovery_iprange',
  232. help=('Temporary IP range that will be given to nodes during '
  233. 'the inspection process. Should not overlap with the '
  234. 'range defined by dhcp_start and dhcp_end, but should '
  235. 'be in the same network.')
  236. ),
  237. cfg.BoolOpt('inspection_extras',
  238. default=True,
  239. help=('Whether to enable extra hardware collection during '
  240. 'the inspection process. Requires python-hardware or '
  241. 'python-hardware-detect package on the introspection '
  242. 'image.')),
  243. cfg.BoolOpt('inspection_runbench',
  244. default=False,
  245. deprecated_name='discovery_runbench',
  246. help=('Whether to run benchmarks when inspecting nodes. '
  247. 'Requires inspection_extras set to True.')
  248. ),
  249. cfg.BoolOpt('inspection_enable_uefi',
  250. default=True,
  251. help=('Whether to support introspection of nodes that have '
  252. 'UEFI-only firmware.')
  253. ),
  254. cfg.BoolOpt('enable_node_discovery',
  255. default=False,
  256. help=('Makes ironic-inspector enroll any unknown node that '
  257. 'PXE-boots introspection ramdisk in Ironic. By default, '
  258. 'the "fake" driver is used for new nodes (it is '
  259. 'automatically enabled when this option is set to True).'
  260. ' Set discovery_default_driver to override. '
  261. 'Introspection rules can also be used to specify driver '
  262. 'information for newly enrolled nodes.')
  263. ),
  264. cfg.StrOpt('discovery_default_driver',
  265. default='pxe_ipmitool',
  266. help=('The default driver to use for newly discovered nodes '
  267. '(requires enable_node_discovery set to True). This '
  268. 'driver is automatically added to enabled_drivers.')
  269. ),
  270. cfg.BoolOpt('undercloud_debug',
  271. default=True,
  272. help=('Whether to enable the debug log level for Undercloud '
  273. 'OpenStack services.')
  274. ),
  275. cfg.BoolOpt('undercloud_update_packages',
  276. default=True,
  277. help=('Whether to update packages during the Undercloud '
  278. 'install.')
  279. ),
  280. cfg.BoolOpt('enable_tempest',
  281. default=True,
  282. help=('Whether to install Tempest in the Undercloud.')
  283. ),
  284. cfg.BoolOpt('enable_telemetry',
  285. default=False,
  286. help=('Whether to install Telemetry services '
  287. '(ceilometer, gnocchi, aodh, panko ) in the Undercloud.')
  288. ),
  289. cfg.BoolOpt('enable_ui',
  290. default=True,
  291. help=('Whether to install the TripleO UI.')
  292. ),
  293. cfg.BoolOpt('enable_validations',
  294. default=True,
  295. help=('Whether to install requirements to run the TripleO '
  296. 'validations.')
  297. ),
  298. cfg.BoolOpt('enable_cinder',
  299. default=False,
  300. help=('Whether to install the Volume service. It is not '
  301. 'currently used in the undercloud.')),
  302. cfg.BoolOpt('enable_legacy_ceilometer_api',
  303. default=False,
  304. help=('Whether to enable legacy ceilometer api '
  305. 'in the Undercloud. '
  306. 'Note: Ceilometer API has been deprecated and will be '
  307. 'removed in future release. Please consider moving to '
  308. 'gnocchi/Aodh/Panko API instead.')
  309. ),
  310. cfg.BoolOpt('enable_legacy_ceilometer_collector',
  311. default=False,
  312. help=('Whether to enable legacy ceilometer collector '
  313. 'in the Undercloud. '
  314. 'Note: Ceilometer collector has been deprecated and '
  315. 'will be removed in future release.')
  316. ),
  317. cfg.BoolOpt('enable_novajoin',
  318. default=False,
  319. help=('Whether to install novajoin metadata service in '
  320. 'the Undercloud.')
  321. ),
  322. cfg.BoolOpt('enable_container_images_build',
  323. default=True,
  324. help=('Whether to enable docker container images to be build '
  325. 'on the undercloud.')
  326. ),
  327. cfg.StrOpt('ipa_otp',
  328. default='',
  329. help=('One Time Password to register Undercloud node with '
  330. 'an IPA server. '
  331. 'Required when enable_novajoin = True.')
  332. ),
  333. cfg.BoolOpt('ipxe_enabled',
  334. default=True,
  335. help=('Whether to use iPXE for deploy and inspection.'),
  336. deprecated_name='ipxe_deploy',
  337. ),
  338. cfg.IntOpt('scheduler_max_attempts',
  339. default=30, min=1,
  340. help=('Maximum number of attempts the scheduler will make '
  341. 'when deploying the instance. You should keep it '
  342. 'greater or equal to the number of bare metal nodes '
  343. 'you expect to deploy at once to work around '
  344. 'potential race condition when scheduling.')),
  345. cfg.BoolOpt('clean_nodes',
  346. default=False,
  347. help=('Whether to clean overcloud nodes (wipe the hard drive) '
  348. 'between deployments and after the introspection.')),
  349. cfg.ListOpt('enabled_drivers',
  350. default=['pxe_ipmitool', 'pxe_drac', 'pxe_ilo'],
  351. help=('List of enabled bare metal drivers.')),
  352. cfg.ListOpt('enabled_hardware_types',
  353. default=['ipmi', 'redfish'],
  354. help=('List of enabled bare metal hardware types (next '
  355. 'generation drivers).')),
  356. cfg.StrOpt('docker_registry_mirror',
  357. default='',
  358. help=('An optional docker \'registry-mirror\' that will be'
  359. 'configured in /etc/docker/daemon.json.')
  360. ),
  361. ]
  362. # Passwords, tokens, hashes
  363. _auth_opts = [
  364. cfg.StrOpt('undercloud_db_password',
  365. help=('Password used for MySQL databases. '
  366. 'If left unset, one will be automatically generated.')
  367. ),
  368. cfg.StrOpt('undercloud_admin_token',
  369. help=('Keystone admin token. '
  370. 'If left unset, one will be automatically generated.')
  371. ),
  372. cfg.StrOpt('undercloud_admin_password',
  373. help=('Keystone admin password. '
  374. 'If left unset, one will be automatically generated.')
  375. ),
  376. cfg.StrOpt('undercloud_glance_password',
  377. help=('Glance service password. '
  378. 'If left unset, one will be automatically generated.')
  379. ),
  380. cfg.StrOpt('undercloud_heat_encryption_key',
  381. help=('Heat db encryption key(must be 16, 24, or 32 characters.'
  382. ' If left unset, one will be automatically generated.')
  383. ),
  384. cfg.StrOpt('undercloud_heat_password',
  385. help=('Heat service password. '
  386. 'If left unset, one will be automatically generated.')
  387. ),
  388. cfg.StrOpt('undercloud_heat_cfn_password',
  389. help=('Heat cfn service password. '
  390. 'If left unset, one will be automatically generated.')
  391. ),
  392. cfg.StrOpt('undercloud_neutron_password',
  393. help=('Neutron service password. '
  394. 'If left unset, one will be automatically generated.')
  395. ),
  396. cfg.StrOpt('undercloud_nova_password',
  397. help=('Nova service password. '
  398. 'If left unset, one will be automatically generated.')
  399. ),
  400. cfg.StrOpt('undercloud_ironic_password',
  401. help=('Ironic service password. '
  402. 'If left unset, one will be automatically generated.')
  403. ),
  404. cfg.StrOpt('undercloud_aodh_password',
  405. help=('Aodh service password. '
  406. 'If left unset, one will be automatically generated.')
  407. ),
  408. cfg.StrOpt('undercloud_gnocchi_password',
  409. help=('Gnocchi service password. '
  410. 'If left unset, one will be automatically generated.')
  411. ),
  412. cfg.StrOpt('undercloud_ceilometer_password',
  413. help=('Ceilometer service password. '
  414. 'If left unset, one will be automatically generated.')
  415. ),
  416. cfg.StrOpt('undercloud_panko_password',
  417. help=('Panko service password. '
  418. 'If left unset, one will be automatically generated.')
  419. ),
  420. cfg.StrOpt('undercloud_ceilometer_metering_secret',
  421. help=('Ceilometer metering secret. '
  422. 'If left unset, one will be automatically generated.')
  423. ),
  424. cfg.StrOpt('undercloud_ceilometer_snmpd_user',
  425. default='ro_snmp_user',
  426. help=('Ceilometer snmpd read-only user. If this value is '
  427. 'changed from the default, the new value must be passed '
  428. 'in the overcloud environment as the parameter '
  429. 'SnmpdReadonlyUserName. This value must be between '
  430. '1 and 32 characters long.')
  431. ),
  432. cfg.StrOpt('undercloud_ceilometer_snmpd_password',
  433. help=('Ceilometer snmpd password. '
  434. 'If left unset, one will be automatically generated.')
  435. ),
  436. cfg.StrOpt('undercloud_swift_password',
  437. help=('Swift service password. '
  438. 'If left unset, one will be automatically generated.')
  439. ),
  440. cfg.StrOpt('undercloud_mistral_password',
  441. help=('Mistral service password. '
  442. 'If left unset, one will be automatically generated.')
  443. ),
  444. cfg.StrOpt('undercloud_rabbit_cookie',
  445. help=('Rabbitmq cookie. '
  446. 'If left unset, one will be automatically generated.')
  447. ),
  448. cfg.StrOpt('undercloud_rabbit_password',
  449. help=('Rabbitmq password. '
  450. 'If left unset, one will be automatically generated.')
  451. ),
  452. cfg.StrOpt('undercloud_rabbit_username',
  453. help=('Rabbitmq username. '
  454. 'If left unset, one will be automatically generated.')
  455. ),
  456. cfg.StrOpt('undercloud_heat_stack_domain_admin_password',
  457. help=('Heat stack domain admin password. '
  458. 'If left unset, one will be automatically generated.')
  459. ),
  460. cfg.StrOpt('undercloud_swift_hash_suffix',
  461. help=('Swift hash suffix. '
  462. 'If left unset, one will be automatically generated.')
  463. ),
  464. cfg.StrOpt('undercloud_haproxy_stats_password',
  465. help=('HAProxy stats password. '
  466. 'If left unset, one will be automatically generated.')
  467. ),
  468. cfg.StrOpt('undercloud_zaqar_password',
  469. help=('Zaqar password. '
  470. 'If left unset, one will be automatically generated.')
  471. ),
  472. cfg.StrOpt('undercloud_horizon_secret_key',
  473. help=('Horizon secret key. '
  474. 'If left unset, one will be automatically generated.')
  475. ),
  476. cfg.StrOpt('undercloud_cinder_password',
  477. help=('Cinder service password. '
  478. 'If left unset, one will be automatically generated.')
  479. ),
  480. cfg.StrOpt('undercloud_novajoin_password',
  481. help=('Novajoin vendordata plugin service password. '
  482. 'If left unset, one will be automatically generated.')
  483. ),
  484. ]
  485. CONF.register_opts(_opts)
  486. CONF.register_opts(_auth_opts, group='auth')
  487. def list_opts():
  488. return [(None, copy.deepcopy(_opts)),
  489. ('auth', copy.deepcopy(_auth_opts)),
  490. ]
  491. def _configure_logging(level, filename):
  492. """Does the initial logging configuration
  493. This should only ever be called once. If further changes to the logging
  494. config are needed they should be made directly on the LOG object.
  495. :param level: The desired logging level
  496. :param filename: The log file. Set to None to disable file logging.
  497. """
  498. try:
  499. os.makedirs(os.path.dirname(PATHS.LOG_FILE))
  500. except OSError as e:
  501. if e.errno != errno.EEXIST:
  502. raise
  503. logging.basicConfig(filename=filename,
  504. format=DEFAULT_LOG_FORMAT,
  505. level=level)
  506. global LOG
  507. LOG = logging.getLogger(__name__)
  508. if os.environ.get('OS_LOG_CAPTURE') != '1':
  509. handler = logging.StreamHandler()
  510. formatter = logging.Formatter(DEFAULT_LOG_FORMAT)
  511. handler.setFormatter(formatter)
  512. LOG.addHandler(handler)
  513. def _load_config():
  514. conf_params = []
  515. if os.path.isfile(PATHS.PASSWORD_PATH):
  516. conf_params += ['--config-file', PATHS.PASSWORD_PATH]
  517. if os.path.isfile(PATHS.CONF_PATH):
  518. conf_params += ['--config-file', PATHS.CONF_PATH]
  519. CONF(conf_params)
  520. def _run_command(args, env=None, name=None):
  521. """Run the command defined by args and return its output
  522. :param args: List of arguments for the command to be run.
  523. :param env: Dict defining the environment variables. Pass None to use
  524. the current environment.
  525. :param name: User-friendly name for the command being run. A value of
  526. None will cause args[0] to be used.
  527. """
  528. if name is None:
  529. name = args[0]
  530. try:
  531. return subprocess.check_output(args,
  532. stderr=subprocess.STDOUT,
  533. env=env).decode('utf-8')
  534. except subprocess.CalledProcessError as e:
  535. LOG.error('%s failed: %s', name, e.output)
  536. raise
  537. def _run_live_command(args, env=None, name=None):
  538. """Run the command defined by args and log its output
  539. Takes the same arguments as _run_command, but runs the process
  540. asynchronously so the output can be logged while the process is still
  541. running.
  542. """
  543. if name is None:
  544. name = args[0]
  545. process = subprocess.Popen(args, env=env,
  546. stdout=subprocess.PIPE,
  547. stderr=subprocess.STDOUT)
  548. while True:
  549. line = process.stdout.readline().decode('utf-8')
  550. if line:
  551. LOG.info(line.rstrip())
  552. if line == '' and process.poll() is not None:
  553. break
  554. if process.returncode != 0:
  555. raise RuntimeError('%s failed. See log for details.' % name)
  556. def _check_hostname():
  557. """Check system hostname configuration
  558. Rabbit and Puppet require pretty specific hostname configuration. This
  559. function ensures that the system hostname settings are valid before
  560. continuing with the installation.
  561. """
  562. if CONF.undercloud_hostname is not None:
  563. args = ['sudo', 'hostnamectl', 'set-hostname',
  564. CONF.undercloud_hostname]
  565. _run_command(args, name='hostnamectl')
  566. LOG.info('Checking for a FQDN hostname...')
  567. args = ['sudo', 'hostnamectl', '--static']
  568. detected_static_hostname = _run_command(args, name='hostnamectl').rstrip()
  569. LOG.info('Static hostname detected as %s', detected_static_hostname)
  570. args = ['sudo', 'hostnamectl', '--transient']
  571. detected_transient_hostname = _run_command(args,
  572. name='hostnamectl').rstrip()
  573. LOG.info('Transient hostname detected as %s', detected_transient_hostname)
  574. if detected_static_hostname != detected_transient_hostname:
  575. LOG.error('Static hostname "%s" does not match transient hostname '
  576. '"%s".', detected_static_hostname,
  577. detected_transient_hostname)
  578. LOG.error('Use hostnamectl to set matching hostnames.')
  579. raise RuntimeError('Static and transient hostnames do not match')
  580. with open('/etc/hosts') as hosts_file:
  581. for line in hosts_file:
  582. if (not line.lstrip().startswith('#') and
  583. detected_static_hostname in line.split()):
  584. break
  585. else:
  586. short_hostname = detected_static_hostname.split('.')[0]
  587. if short_hostname == detected_static_hostname:
  588. raise RuntimeError('Configured hostname is not fully '
  589. 'qualified.')
  590. echo_cmd = ('echo 127.0.0.1 %s %s >> /etc/hosts' %
  591. (detected_static_hostname, short_hostname))
  592. args = ['sudo', '/bin/bash', '-c', echo_cmd]
  593. _run_command(args, name='hostname-to-etc-hosts')
  594. LOG.info('Added hostname %s to /etc/hosts',
  595. detected_static_hostname)
  596. def _check_memory():
  597. """Check system memory
  598. The undercloud will not run properly in less than 8 GB of memory.
  599. This function verifies that at least that much is available before
  600. proceeding with install.
  601. """
  602. mem = psutil.virtual_memory()
  603. swap = psutil.swap_memory()
  604. total_mb = (mem.total + swap.total) / 1024 / 1024
  605. if total_mb < REQUIRED_MB:
  606. LOG.error('At least %d MB of memory is required for undercloud '
  607. 'installation. A minimum of 8 GB is recommended. '
  608. 'Only detected %d MB' % (REQUIRED_MB, total_mb))
  609. raise RuntimeError('Insufficient memory available')
  610. def _check_ipv6_enabled():
  611. """Test if IPv6 is enabled
  612. If /proc/net/if_inet6 exist ipv6 sysctl settings are available.
  613. """
  614. return os.path.isfile('/proc/net/if_inet6')
  615. def _check_sysctl():
  616. """Check sysctl option availability
  617. The undercloud will not install properly if some of the expected sysctl
  618. values are not available to be set.
  619. """
  620. options = ['net.ipv4.ip_forward', 'net.ipv4.ip_nonlocal_bind']
  621. if _check_ipv6_enabled():
  622. options.append('net.ipv6.ip_nonlocal_bind')
  623. not_available = []
  624. for option in options:
  625. path = '/proc/sys/{opt}'.format(opt=option.replace('.', '/'))
  626. if not os.path.isfile(path):
  627. not_available.append(option)
  628. if not_available:
  629. LOG.error('Required sysctl options are not available. Check '
  630. 'that your kernel is up to date. Missing: '
  631. '{options}'.format(options=", ".join(not_available)))
  632. raise RuntimeError('Missing sysctl options')
  633. def _validate_network():
  634. def error_handler(message):
  635. LOG.error('Undercloud configuration validation failed: %s', message)
  636. raise validator.FailedValidation(message)
  637. params = {opt.name: CONF[opt.name] for opt in _opts}
  638. validator.validate_config(params, error_handler)
  639. def _validate_no_ip_change():
  640. """Disallow provisioning interface IP changes
  641. Changing the provisioning network IP causes a number of issues, so we
  642. need to disallow it early in the install before configurations start to
  643. be changed.
  644. """
  645. os_net_config_file = '/etc/os-net-config/config.json'
  646. # Nothing to do if we haven't already installed
  647. if not os.path.isfile(
  648. os.path.expanduser(os_net_config_file)):
  649. return
  650. with open(os_net_config_file) as f:
  651. network_config = json.loads(f.read())
  652. try:
  653. ctlplane = [i for i in network_config.get('network_config', [])
  654. if i['name'] == 'br-ctlplane'][0]
  655. except IndexError:
  656. # Nothing to check if br-ctlplane wasn't configured
  657. return
  658. existing_ip = ctlplane['addresses'][0]['ip_netmask']
  659. if existing_ip != CONF.local_ip:
  660. message = ('Changing the local_ip is not allowed. Existing IP: '
  661. '%s, Configured IP: %s') % (existing_ip,
  662. CONF.network_cidr)
  663. LOG.error(message)
  664. raise validator.FailedValidation(message)
  665. def _validate_configuration():
  666. try:
  667. _check_hostname()
  668. _check_memory()
  669. _check_sysctl()
  670. _validate_network()
  671. _validate_no_ip_change()
  672. except RuntimeError as e:
  673. LOG.error('An error occurred during configuration validation, '
  674. 'please check your host configuration and try again. '
  675. 'Error message: {error}'.format(error=e))
  676. sys.exit(1)
  677. def _generate_password(length=40):
  678. """Create a random password
  679. Copied from rdomanager-oscplugin. This should eventually live in
  680. tripleo-common.
  681. """
  682. uuid_str = six.text_type(uuid.uuid4()).encode("UTF-8")
  683. return hashlib.sha1(uuid_str).hexdigest()[:length]
  684. def _get_service_endpoints(name, format_str, public, internal, admin=None,
  685. public_proto='http', internal_proto='http'):
  686. endpoints = {}
  687. upper_name = name.upper().replace('-', '_')
  688. public_port_key = 'port'
  689. if not admin:
  690. admin = internal
  691. if public_proto in ['https', 'wss']:
  692. public_port_key = 'ssl_port'
  693. endpoints['UNDERCLOUD_ENDPOINT_%s_PUBLIC' % upper_name] = (
  694. format_str % (public_proto, public['host'], public[public_port_key]))
  695. endpoints['UNDERCLOUD_ENDPOINT_%s_INTERNAL' % upper_name] = (
  696. format_str % (internal_proto, internal['host'], internal['port']))
  697. endpoints['UNDERCLOUD_ENDPOINT_%s_ADMIN' % upper_name] = (
  698. format_str % (internal_proto, admin['host'], admin['port']))
  699. return endpoints
  700. def _generate_endpoints(instack_env):
  701. local_host = instack_env['LOCAL_IP']
  702. public_host = local_host
  703. public_proto = 'http'
  704. internal_host = local_host
  705. internal_proto = 'http'
  706. zaqar_ws_public_proto = 'ws'
  707. zaqar_ws_internal_proto = 'ws'
  708. if (CONF.undercloud_service_certificate or
  709. CONF.generate_service_certificate):
  710. public_host = CONF.undercloud_public_host
  711. internal_host = CONF.undercloud_admin_host
  712. public_proto = 'https'
  713. zaqar_ws_public_proto = 'wss'
  714. endpoints = {}
  715. endpoint_list = [
  716. ('heat',
  717. '%s://%s:%d/v1/%%(tenant_id)s',
  718. {'host': public_host, 'port': 8004, 'ssl_port': 13004},
  719. {'host': internal_host, 'port': 8004}),
  720. ('heat-cfn',
  721. '%s://%s:%d/v1/%%(tenant_id)s',
  722. {'host': public_host, 'port': 8000, 'ssl_port': 13800},
  723. {'host': internal_host, 'port': 8000}),
  724. ('heat-ui-proxy',
  725. '%s://%s:%d',
  726. {'host': public_host, 'port': 8004, 'ssl_port': 13004},
  727. {'host': internal_host, 'port': 8004}),
  728. ('heat-ui-config',
  729. '%s://%s:%d/heat/v1/%%(project_id)s',
  730. {'host': public_host, 'port': 3000, 'ssl_port': 443},
  731. {'host': internal_host, 'port': 3000}),
  732. ('neutron',
  733. '%s://%s:%d',
  734. {'host': public_host, 'port': 9696, 'ssl_port': 13696},
  735. {'host': internal_host, 'port': 9696}),
  736. ('glance',
  737. '%s://%s:%d',
  738. {'host': public_host, 'port': 9292, 'ssl_port': 13292},
  739. {'host': internal_host, 'port': 9292}),
  740. ('nova',
  741. '%s://%s:%d/v2.1',
  742. {'host': public_host, 'port': 8774, 'ssl_port': 13774},
  743. {'host': internal_host, 'port': 8774}),
  744. ('placement',
  745. '%s://%s:%d/placement',
  746. {'host': public_host, 'port': 8778, 'ssl_port': 13778},
  747. {'host': internal_host, 'port': 8778}),
  748. ('ceilometer',
  749. '%s://%s:%d',
  750. {'host': public_host, 'port': 8777, 'ssl_port': 13777},
  751. {'host': internal_host, 'port': 8777}),
  752. ('keystone',
  753. '%s://%s:%d',
  754. {'host': public_host, 'port': 5000, 'ssl_port': 13000},
  755. {'host': internal_host, 'port': 5000},
  756. {'host': internal_host, 'port': 35357}),
  757. ('keystone-ui-config',
  758. '%s://%s:%d/keystone/v3',
  759. {'host': public_host, 'port': 3000, 'ssl_port': 443},
  760. {'host': internal_host, 'port': 3000},
  761. {'host': internal_host, 'port': 35357}),
  762. ('swift',
  763. '%s://%s:%d/v1/AUTH_%%(tenant_id)s',
  764. {'host': public_host, 'port': 8080, 'ssl_port': 13808},
  765. {'host': internal_host, 'port': 8080}),
  766. ('swift-ui-proxy',
  767. '%s://%s:%d',
  768. {'host': public_host, 'port': 8080, 'ssl_port': 13808},
  769. {'host': internal_host, 'port': 8080}),
  770. ('swift-ui-config',
  771. '%s://%s:%d/swift/v1/AUTH_%%(project_id)s',
  772. {'host': public_host, 'port': 3000, 'ssl_port': 443},
  773. {'host': internal_host, 'port': 3000}),
  774. ('ironic',
  775. '%s://%s:%d',
  776. {'host': public_host, 'port': 6385, 'ssl_port': 13385},
  777. {'host': internal_host, 'port': 6385}),
  778. ('ironic-ui-config',
  779. '%s://%s:%d/ironic',
  780. {'host': public_host, 'port': 3000, 'ssl_port': 443},
  781. {'host': internal_host, 'port': 3000}),
  782. ('ironic_inspector',
  783. '%s://%s:%d',
  784. {'host': public_host, 'port': 5050, 'ssl_port': 13050},
  785. {'host': internal_host, 'port': 5050}),
  786. ('aodh',
  787. '%s://%s:%d',
  788. {'host': public_host, 'port': 8042, 'ssl_port': 13042},
  789. {'host': internal_host, 'port': 8042}),
  790. ('gnocchi',
  791. '%s://%s:%d',
  792. {'host': public_host, 'port': 8041, 'ssl_port': 13041},
  793. {'host': internal_host, 'port': 8041}),
  794. ('panko',
  795. '%s://%s:%d',
  796. {'host': public_host, 'port': 8779, 'ssl_port': 13779},
  797. {'host': internal_host, 'port': 8779}),
  798. ('mistral',
  799. '%s://%s:%d/v2',
  800. {'host': public_host, 'port': 8989, 'ssl_port': 13989},
  801. {'host': internal_host, 'port': 8989}),
  802. ('mistral-ui-proxy',
  803. '%s://%s:%d',
  804. {'host': public_host, 'port': 8989, 'ssl_port': 13989},
  805. {'host': internal_host, 'port': 8989}),
  806. ('mistral-ui-config',
  807. '%s://%s:%d/mistral/v2',
  808. {'host': public_host, 'port': 3000, 'ssl_port': 443},
  809. {'host': internal_host, 'port': 3000}),
  810. ('zaqar',
  811. '%s://%s:%d',
  812. {'host': public_host, 'port': 8888, 'ssl_port': 13888},
  813. {'host': internal_host, 'port': 8888}),
  814. ('cinder',
  815. '%s://%s:%d/v1/%%(tenant_id)s',
  816. {'host': public_host, 'port': 8776, 'ssl_port': 13776},
  817. {'host': internal_host, 'port': 8776}),
  818. ('cinder_v2',
  819. '%s://%s:%d/v2/%%(tenant_id)s',
  820. {'host': public_host, 'port': 8776, 'ssl_port': 13776},
  821. {'host': internal_host, 'port': 8776}),
  822. ('cinder_v3',
  823. '%s://%s:%d/v3/%%(tenant_id)s',
  824. {'host': public_host, 'port': 8776, 'ssl_port': 13776},
  825. {'host': internal_host, 'port': 8776}),
  826. ]
  827. for endpoint_data in endpoint_list:
  828. endpoints.update(
  829. _get_service_endpoints(*endpoint_data,
  830. public_proto=public_proto,
  831. internal_proto=internal_proto))
  832. # Zaqar's websocket endpoint
  833. # NOTE(jaosorior): Zaqar's websocket endpoint doesn't support being proxied
  834. # on a different port. If that's done it will ignore the handshake and
  835. # won't work.
  836. endpoints.update(_get_service_endpoints(
  837. 'zaqar-websocket',
  838. '%s://%s:%d',
  839. {'host': public_host, 'port': 9000, 'ssl_port': 9000},
  840. {'host': internal_host, 'port': 9000},
  841. public_proto=zaqar_ws_public_proto,
  842. internal_proto=zaqar_ws_internal_proto))
  843. endpoints.update(_get_service_endpoints(
  844. 'zaqar-ui-proxy',
  845. '%s://%s:%d',
  846. {'host': public_host, 'port': 9000, 'ssl_port': 443,
  847. 'zaqar_ws_public_proto': 'ws'},
  848. {'host': internal_host, 'port': 9000},
  849. public_proto=zaqar_ws_public_proto,
  850. internal_proto=zaqar_ws_internal_proto))
  851. endpoints.update(_get_service_endpoints(
  852. 'zaqar-ui-config',
  853. '%s://%s:%d/zaqar',
  854. {'host': public_host, 'port': 3000, 'ssl_port': 443,
  855. 'zaqar_ws_public_proto': 'wss'},
  856. {'host': internal_host, 'port': 3000},
  857. public_proto=zaqar_ws_public_proto,
  858. internal_proto=zaqar_ws_internal_proto))
  859. # The swift admin endpoint has a different format from the others
  860. endpoints['UNDERCLOUD_ENDPOINT_SWIFT_ADMIN'] = (
  861. '%s://%s:%s' % (internal_proto, internal_host, 8080))
  862. instack_env.update(endpoints)
  863. def _write_password_file(instack_env):
  864. with open(PATHS.PASSWORD_PATH, 'w') as password_file:
  865. password_file.write('[auth]\n')
  866. for opt in _auth_opts:
  867. env_name = opt.name.upper()
  868. value = CONF.auth[opt.name]
  869. if not value:
  870. # Heat requires this encryption key to be a specific length
  871. if env_name == 'UNDERCLOUD_HEAT_ENCRYPTION_KEY':
  872. value = _generate_password(32)
  873. else:
  874. value = _generate_password()
  875. LOG.info('Generated new password for %s', opt.name)
  876. instack_env[env_name] = value
  877. password_file.write('%s=%s\n' % (opt.name, value))
  878. os.chmod(PATHS.PASSWORD_PATH, 0o600)
  879. def _member_role_exists():
  880. # This is a workaround for puppet removing the deprecated _member_
  881. # role on upgrade - if it exists we must restore role assignments
  882. # or trusts stored in the undercloud heat will break
  883. user, password, project, auth_url = _get_auth_values()
  884. auth_kwargs = {
  885. 'auth_url': auth_url,
  886. 'username': user,
  887. 'password': password,
  888. 'project_name': project,
  889. 'project_domain_name': 'Default',
  890. 'user_domain_name': 'Default',
  891. }
  892. auth_plugin = ks_auth.Password(**auth_kwargs)
  893. sess = session.Session(auth=auth_plugin)
  894. disc = discover.Discover(session=sess)
  895. c = disc.create_client()
  896. try:
  897. member_role = [r for r in c.roles.list() if r.name == '_member_'][0]
  898. except IndexError:
  899. # Do nothing if there is no _member_ role
  900. return
  901. if c.version == 'v2.0':
  902. client_projects = c.tenants
  903. else:
  904. client_projects = c.projects
  905. admin_project = [t for t in client_projects.list() if t.name == 'admin'][0]
  906. admin_user = [u for u in c.users.list() if u.name == 'admin'][0]
  907. if c.version == 'v2.0':
  908. try:
  909. c.roles.add_user_role(admin_user, member_role, admin_project.id)
  910. LOG.info('Added _member_ role to admin user')
  911. except ks_exceptions.http.Conflict:
  912. # They already had the role
  913. pass
  914. else:
  915. try:
  916. c.roles.grant(member_role,
  917. user=admin_user,
  918. project=admin_project.id)
  919. LOG.info('Added _member_ role to admin user')
  920. except ks_exceptions.http.Conflict:
  921. # They already had the role
  922. pass
  923. class InstackEnvironment(dict):
  924. """An environment to pass to Puppet with some safety checks.
  925. Keeps lists of variables we add to the operating system environment,
  926. and ensures that we don't anything not defined there.
  927. """
  928. INSTACK_KEYS = {'HOSTNAME', 'ELEMENTS_PATH', 'NODE_DIST', 'JSONFILE',
  929. 'REG_METHOD', 'REG_HALT_UNREGISTER', 'PUBLIC_INTERFACE_IP'}
  930. """The variables instack and/or used elements can read."""
  931. DYNAMIC_KEYS = {'INSPECTION_COLLECTORS', 'INSPECTION_KERNEL_ARGS',
  932. 'INSPECTION_NODE_NOT_FOUND_HOOK',
  933. 'TRIPLEO_INSTALL_USER', 'TRIPLEO_UNDERCLOUD_CONF_FILE',
  934. 'TRIPLEO_UNDERCLOUD_PASSWORD_FILE',
  935. 'ENABLED_POWER_INTERFACES',
  936. 'ENABLED_MANAGEMENT_INTERFACES', 'SYSCTL_SETTINGS'}
  937. """The variables we calculate in _generate_environment call."""
  938. PUPPET_KEYS = DYNAMIC_KEYS | {opt.name.upper() for _, group in list_opts()
  939. for opt in group}
  940. """Keys we pass for formatting the resulting hieradata."""
  941. SET_ALLOWED_KEYS = DYNAMIC_KEYS | INSTACK_KEYS | PUPPET_KEYS
  942. """Keys which we allow to add/change in this environment."""
  943. def __init__(self):
  944. super(InstackEnvironment, self).__init__(os.environ)
  945. def __setitem__(self, key, value):
  946. if key not in self.SET_ALLOWED_KEYS:
  947. raise KeyError('Key %s is not allowed for an InstackEnvironment' %
  948. key)
  949. return super(InstackEnvironment, self).__setitem__(key, value)
  950. def _make_list(values):
  951. """Generate a list suitable to pass to templates."""
  952. return '[%s]' % ', '.join('"%s"' % item for item in values)
  953. def _generate_sysctl_settings():
  954. sysctl_settings = {}
  955. sysctl_settings.update({"net.ipv4.ip_nonlocal_bind": {"value": 1}})
  956. if _check_ipv6_enabled():
  957. sysctl_settings.update({"net.ipv6.ip_nonlocal_bind": {"value": 1}})
  958. return json.dumps(sysctl_settings)
  959. def _generate_environment(instack_root):
  960. """Generate an environment dict for instack
  961. The returned dict will have the necessary values for use as the env
  962. parameter when calling instack via the subprocess module.
  963. :param instack_root: The path containing the instack-undercloud elements
  964. and json files.
  965. """
  966. instack_env = InstackEnvironment()
  967. # Rabbit uses HOSTNAME, so we need to make sure it's right
  968. instack_env['HOSTNAME'] = CONF.undercloud_hostname or socket.gethostname()
  969. # Find the paths we need
  970. json_file_dir = '/usr/share/instack-undercloud/json-files'
  971. if not os.path.isdir(json_file_dir):
  972. json_file_dir = os.path.join(instack_root, 'json-files')
  973. instack_undercloud_elements = '/usr/share/instack-undercloud'
  974. if not os.path.isdir(instack_undercloud_elements):
  975. instack_undercloud_elements = os.path.join(instack_root, 'elements')
  976. tripleo_puppet_elements = '/usr/share/tripleo-puppet-elements'
  977. if not os.path.isdir(tripleo_puppet_elements):
  978. tripleo_puppet_elements = os.path.join(os.getcwd(),
  979. 'tripleo-puppet-elements',
  980. 'elements')
  981. if 'ELEMENTS_PATH' in os.environ:
  982. instack_env['ELEMENTS_PATH'] = os.environ['ELEMENTS_PATH']
  983. else:
  984. instack_env['ELEMENTS_PATH'] = (
  985. '%s:%s:'
  986. '/usr/share/tripleo-image-elements:'
  987. '/usr/share/diskimage-builder/elements'
  988. ) % (tripleo_puppet_elements, instack_undercloud_elements)
  989. # Distro-specific values
  990. distro = platform.linux_distribution()[0]
  991. if distro.startswith('Red Hat Enterprise Linux'):
  992. instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'rhel7'
  993. instack_env['JSONFILE'] = (
  994. os.environ.get('JSONFILE') or
  995. os.path.join(json_file_dir, 'rhel-7-undercloud-packages.json')
  996. )
  997. instack_env['REG_METHOD'] = 'disable'
  998. instack_env['REG_HALT_UNREGISTER'] = '1'
  999. elif distro.startswith('CentOS'):
  1000. instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'centos7'
  1001. instack_env['JSONFILE'] = (
  1002. os.environ.get('JSONFILE') or
  1003. os.path.join(json_file_dir, 'centos-7-undercloud-packages.json')
  1004. )
  1005. elif distro.startswith('Fedora'):
  1006. instack_env['NODE_DIST'] = os.environ.get('NODE_DIST') or 'fedora'
  1007. raise RuntimeError('Fedora is not currently supported')
  1008. else:
  1009. raise RuntimeError('%s is not supported' % distro)
  1010. # Convert conf opts to env values
  1011. for opt in _opts:
  1012. env_name = opt.name.upper()
  1013. instack_env[env_name] = six.text_type(CONF[opt.name])
  1014. # Opts that needs extra processing
  1015. if CONF.inspection_runbench and not CONF.inspection_extras:
  1016. raise RuntimeError('inspection_extras must be enabled for '
  1017. 'inspection_runbench to work')
  1018. if CONF.inspection_extras:
  1019. instack_env['INSPECTION_COLLECTORS'] = 'default,extra-hardware,logs'
  1020. else:
  1021. instack_env['INSPECTION_COLLECTORS'] = 'default,logs'
  1022. inspection_kernel_args = []
  1023. if CONF.undercloud_debug:
  1024. inspection_kernel_args.append('ipa-debug=1')
  1025. if CONF.inspection_runbench:
  1026. inspection_kernel_args.append('ipa-inspection-benchmarks=cpu,mem,disk')
  1027. if CONF.inspection_extras:
  1028. inspection_kernel_args.append('ipa-inspection-dhcp-all-interfaces=1')
  1029. inspection_kernel_args.append('ipa-collect-lldp=1')
  1030. instack_env['INSPECTION_KERNEL_ARGS'] = ' '.join(inspection_kernel_args)
  1031. # Ensure correct rendering of the list and uniqueness of the items
  1032. enabled_drivers = set(CONF.enabled_drivers)
  1033. enabled_hardware_types = set(CONF.enabled_hardware_types)
  1034. if CONF.enable_node_discovery:
  1035. if (CONF.discovery_default_driver not in (enabled_drivers |
  1036. enabled_hardware_types)):
  1037. enabled_drivers.add(CONF.discovery_default_driver)
  1038. instack_env['INSPECTION_NODE_NOT_FOUND_HOOK'] = 'enroll'
  1039. else:
  1040. instack_env['INSPECTION_NODE_NOT_FOUND_HOOK'] = ''
  1041. # In most cases power and management interfaces are called the same, so we
  1042. # use one variable for them.
  1043. enabled_interfaces = set()
  1044. if 'ipmi' in enabled_hardware_types:
  1045. enabled_interfaces.add('ipmitool')
  1046. if 'redfish' in enabled_hardware_types:
  1047. enabled_interfaces.add('redfish')
  1048. instack_env['ENABLED_DRIVERS'] = _make_list(enabled_drivers)
  1049. instack_env['ENABLED_HARDWARE_TYPES'] = _make_list(enabled_hardware_types)
  1050. enabled_interfaces = _make_list(enabled_interfaces)
  1051. instack_env['ENABLED_POWER_INTERFACES'] = enabled_interfaces
  1052. instack_env['ENABLED_MANAGEMENT_INTERFACES'] = enabled_interfaces
  1053. instack_env['SYSCTL_SETTINGS'] = _generate_sysctl_settings()
  1054. if CONF.docker_registry_mirror:
  1055. instack_env['DOCKER_REGISTRY_MIRROR'] = CONF.docker_registry_mirror
  1056. instack_env['PUBLIC_INTERFACE_IP'] = instack_env['LOCAL_IP']
  1057. instack_env['LOCAL_IP'] = instack_env['LOCAL_IP'].split('/')[0]
  1058. if instack_env['UNDERCLOUD_SERVICE_CERTIFICATE']:
  1059. instack_env['UNDERCLOUD_SERVICE_CERTIFICATE'] = os.path.abspath(
  1060. instack_env['UNDERCLOUD_SERVICE_CERTIFICATE'])
  1061. # We're not in a chroot so this doesn't make sense, and it causes weird
  1062. # errors if it's set.
  1063. if instack_env.get('DIB_YUM_REPO_CONF'):
  1064. del instack_env['DIB_YUM_REPO_CONF']
  1065. instack_env['TRIPLEO_INSTALL_USER'] = getpass.getuser()
  1066. instack_env['TRIPLEO_UNDERCLOUD_CONF_FILE'] = PATHS.CONF_PATH
  1067. instack_env['TRIPLEO_UNDERCLOUD_PASSWORD_FILE'] = PATHS.PASSWORD_PATH
  1068. # Mustache conditional logic requires ENABLE_NOVAJOIN to be undefined
  1069. # when novajoin is not enabled.
  1070. if instack_env['ENABLE_NOVAJOIN'].lower() == 'false':
  1071. del instack_env['ENABLE_NOVAJOIN']
  1072. _generate_endpoints(instack_env)
  1073. _write_password_file(instack_env)
  1074. if CONF.generate_service_certificate:
  1075. public_host = CONF.undercloud_public_host
  1076. instack_env['UNDERCLOUD_SERVICE_CERTIFICATE'] = (
  1077. '/etc/pki/tls/certs/undercloud-%s.pem' % public_host)
  1078. return instack_env
  1079. def _get_template_path(template):
  1080. local_template_path = os.path.join(
  1081. os.path.dirname(__file__),
  1082. '..',
  1083. 'templates',
  1084. template)
  1085. installed_template_path = os.path.join(
  1086. '/usr/share/instack-undercloud/templates',
  1087. template)
  1088. if os.path.exists(local_template_path):
  1089. return local_template_path
  1090. else:
  1091. return installed_template_path
  1092. def _generate_init_data(instack_env):
  1093. context = instack_env.copy()
  1094. if CONF.hieradata_override:
  1095. hiera_entry = os.path.splitext(
  1096. os.path.basename(CONF.hieradata_override))[0]
  1097. dst = os.path.join('/etc/puppet/hieradata',
  1098. os.path.basename(CONF.hieradata_override))
  1099. _run_command(['sudo', 'mkdir', '-p', '/etc/puppet/hieradata'])
  1100. _run_command(['sudo', 'cp', CONF.hieradata_override, dst])
  1101. _run_command(['sudo', 'chmod', '0644', dst])
  1102. else:
  1103. hiera_entry = ''
  1104. if CONF.net_config_override:
  1105. net_config_json = open(CONF.net_config_override).read()
  1106. else:
  1107. net_config_json = \
  1108. open(_get_template_path('net-config.json.template')).read()
  1109. context['HIERADATA_OVERRIDE'] = hiera_entry
  1110. context['UNDERCLOUD_NAMESERVERS'] = json.dumps(
  1111. CONF.undercloud_nameservers)
  1112. partials = {'net_config': net_config_json}
  1113. renderer = pystache.Renderer(partials=partials)
  1114. template = _get_template_path('config.json.template')
  1115. with open(template) as f:
  1116. config_json = renderer.render(f.read(), context)
  1117. config_json = config_json.replace('&quot;', '"')
  1118. cfn_path = '/var/lib/heat-cfntools/cfn-init-data'
  1119. tmp_json = tempfile.mkstemp()[1]
  1120. with open(tmp_json, 'w') as f:
  1121. print(config_json, file=f)
  1122. if not os.path.exists(os.path.dirname(cfn_path)):
  1123. _run_command(['sudo', 'mkdir', '-p', os.path.dirname(cfn_path)])
  1124. _run_command(['sudo', 'mv', tmp_json, cfn_path])
  1125. _run_command(['sudo', 'chmod', '0644', cfn_path])
  1126. def _run_instack(instack_env):
  1127. args = ['sudo', '-E', 'instack', '-p', instack_env['ELEMENTS_PATH'],
  1128. '-j', instack_env['JSONFILE'],
  1129. ]
  1130. LOG.info('Running instack')
  1131. _run_live_command(args, instack_env, 'instack')
  1132. LOG.info('Instack completed successfully')
  1133. def _run_yum_clean_all(instack_env):
  1134. args = ['sudo', 'yum', 'clean', 'all']
  1135. LOG.info('Running yum clean all')
  1136. _run_live_command(args, instack_env, 'yum-clean-all')
  1137. LOG.info('yum-clean-all completed successfully')
  1138. def _run_yum_update(instack_env):
  1139. args = ['sudo', 'yum', 'update', '-y']
  1140. LOG.info('Running yum update')
  1141. _run_live_command(args, instack_env, 'yum-update')
  1142. LOG.info('yum-update completed successfully')
  1143. def _run_orc(instack_env):
  1144. args = ['sudo', 'os-refresh-config']
  1145. LOG.info('Running os-refresh-config')
  1146. _run_live_command(args, instack_env, 'os-refresh-config')
  1147. LOG.info('os-refresh-config completed successfully')
  1148. def _extract_from_stackrc(name):
  1149. """Extract authentication values from stackrc
  1150. :param name: The value to be extracted. For example: OS_USERNAME or
  1151. OS_AUTH_URL.
  1152. """
  1153. with open(os.path.expanduser('~/stackrc')) as f:
  1154. for line in f:
  1155. if name in line:
  1156. parts = line.split('=')
  1157. return parts[1].rstrip()
  1158. def _ensure_user_identity(id_path):
  1159. if not os.path.isfile(id_path):
  1160. args = ['ssh-keygen', '-t', 'rsa', '-N', '', '-f', id_path]
  1161. _run_command(args)
  1162. LOG.info('Generated new ssh key in ~/.ssh/id_rsa')
  1163. def _get_auth_values():
  1164. """Get auth values from stackrc
  1165. Returns the user, password, project and auth_url as read from stackrc,
  1166. in that order as a tuple.
  1167. """
  1168. user = _extract_from_stackrc('OS_USERNAME')
  1169. password = _run_command(['sudo', 'hiera', 'admin_password']).rstrip()
  1170. project = _extract_from_stackrc('OS_PROJECT_NAME')
  1171. auth_url = _extract_from_stackrc('OS_AUTH_URL')
  1172. return user, password, project, auth_url
  1173. def _configure_ssh_keys(nova):
  1174. """Configure default ssh keypair in Nova
  1175. Generates a new ssh key for the current user if one does not already
  1176. exist, then uploads that to Nova as the 'default' keypair.
  1177. """
  1178. id_path = os.path.expanduser('~/.ssh/id_rsa')
  1179. _ensure_user_identity(id_path)
  1180. try:
  1181. nova.keypairs.get('default')
  1182. except exceptions.NotFound:
  1183. with open(id_path + '.pub') as pubkey:
  1184. nova.keypairs.create('default', pubkey.read().rstrip())
  1185. def _delete_default_flavors(nova):
  1186. """Delete the default flavors from Nova
  1187. The m1.tiny, m1.small, etc. flavors are not useful on an undercloud.
  1188. """
  1189. to_delete = ['m1.tiny', 'm1.small', 'm1.medium', 'm1.large', 'm1.xlarge']
  1190. for f in nova.flavors.list():
  1191. if f.name in to_delete:
  1192. nova.flavors.delete(f.id)
  1193. def _ensure_flavor(nova, name, profile=None):
  1194. try:
  1195. flavor = nova.flavors.create(name, 4096, 1, 40)
  1196. except exceptions.Conflict:
  1197. LOG.info('Not creating flavor "%s" because it already exists.', name)
  1198. return
  1199. keys = {'capabilities:boot_option': 'local'}
  1200. if profile is not None:
  1201. keys['capabilities:profile'] = profile
  1202. flavor.set_keys(keys)
  1203. message = 'Created flavor "%s" with profile "%s"'
  1204. LOG.info(message, name, profile)
  1205. def _copy_stackrc():
  1206. args = ['sudo', 'cp', '/root/stackrc', os.path.expanduser('~')]
  1207. try:
  1208. _run_command(args, name='Copy stackrc')
  1209. except subprocess.CalledProcessError:
  1210. LOG.info("/root/stackrc not found, this is OK on initial deploy")
  1211. args = ['sudo', 'chown', getpass.getuser() + ':',
  1212. os.path.expanduser('~/stackrc')]
  1213. _run_command(args, name='Chown stackrc')
  1214. def _clean_os_refresh_config():
  1215. orc_dirs = glob.glob('/usr/libexec/os-refresh-config/*')
  1216. args = ['sudo', 'rm', '-rf'] + orc_dirs
  1217. _run_command(args, name='Clean os-refresh-config')
  1218. def _clean_os_collect_config():
  1219. occ_dir = '/var/lib/os-collect-config'
  1220. args = ['sudo', 'rm', '-fr', occ_dir]
  1221. _run_command(args, name='Clean os-collect-config')
  1222. def _create_mistral_config_environment(instack_env, mistral):
  1223. # Store the snmpd password in a Mistral environment so it can be accessed
  1224. # by the Mistral actions.
  1225. snmpd_password = instack_env["UNDERCLOUD_CEILOMETER_SNMPD_PASSWORD"]
  1226. env_name = "tripleo.undercloud-config"
  1227. try:
  1228. mistral.environments.get(env_name)
  1229. except ks_exceptions.NotFound:
  1230. mistral.environments.create(
  1231. name=env_name,
  1232. variables=json.dumps({
  1233. "undercloud_ceilometer_snmpd_password": snmpd_password
  1234. }))
  1235. def _migrate_plans(mistral, swift, plans):
  1236. """Migrate plan environments from Mistral to Swift."""
  1237. plan_env_filename = 'plan-environment.yaml'
  1238. for plan in plans:
  1239. headers, objects = swift.get_container(plan)
  1240. if headers.get('x-container-meta-usage-tripleo') != 'plan':
  1241. continue
  1242. try:
  1243. swift.get_object(plan, plan_env_filename)
  1244. except swiftclient.ClientException:
  1245. LOG.info('Migrating environment for plan %s to Swift.' % plan)
  1246. env = mistral.environments.get(plan).variables
  1247. yaml_string = yaml.safe_dump(env, default_flow_style=False)
  1248. swift.put_object(plan, plan_env_filename, yaml_string)
  1249. mistral.environments.delete(plan)
  1250. def _create_default_plan(mistral, plans, timeout=360):
  1251. plan_name = 'overcloud'
  1252. queue_name = str(uuid.uuid4())
  1253. if plan_name in plans:
  1254. LOG.info('Not creating default plan "%s" because it already exists.',
  1255. plan_name)
  1256. return
  1257. execution = mistral.executions.create(
  1258. 'tripleo.plan_management.v1.create_default_deployment_plan',
  1259. workflow_input={'container': plan_name, 'queue_name': queue_name}
  1260. )
  1261. timeout_at = time.time() + timeout
  1262. while time.time() < timeout_at:
  1263. exe = mistral.executions.get(execution.id)
  1264. if exe.state == "RUNNING":
  1265. time.sleep(5)
  1266. continue
  1267. if exe.state == "SUCCESS":
  1268. return
  1269. else:
  1270. raise RuntimeError(
  1271. "Failed to create the default Deployment Plan. Please check "
  1272. "the create_default_deployment_plan execution in Mistral with "
  1273. "`openstack workflow execution list`.")
  1274. else:
  1275. exe = mistral.executions.get(execution.id)
  1276. LOG.error("Timed out waiting for execution %s to finish. State: %s",
  1277. exe.id, exe.state)
  1278. raise RuntimeError(
  1279. "Timed out creating the default Deployment Plan. Please check "
  1280. "the create_default_deployment_plan execution in Mistral with "
  1281. "`openstack workflow execution list`.")
  1282. def _prepare_ssh_environment(mistral):
  1283. mistral.executions.create('tripleo.validations.v1.copy_ssh_key')
  1284. def _post_config_mistral(instack_env, mistral, swift):
  1285. plans = [container["name"] for container in swift.get_account()[1]]
  1286. _create_mistral_config_environment(instack_env, mistral)
  1287. _migrate_plans(mistral, swift, plans)
  1288. _create_default_plan(mistral, plans)
  1289. if CONF.enable_validations:
  1290. _prepare_ssh_environment(mistral)
  1291. def _post_config(instack_env):
  1292. _copy_stackrc()
  1293. user, password, project, auth_url = _get_auth_values()
  1294. auth_kwargs = {
  1295. 'auth_url': auth_url,
  1296. 'username': user,
  1297. 'password': password,
  1298. 'project_name': project,
  1299. 'project_domain_name': 'Default',
  1300. 'user_domain_name': 'Default',
  1301. }
  1302. auth_plugin = ks_auth.Password(**auth_kwargs)
  1303. sess = session.Session(auth=auth_plugin)
  1304. # TODO(andreykurilin): remove this check with support of novaclient 6.0.0
  1305. if nc.__version__[0] == "6":
  1306. nova = novaclient.Client(2, user, password, project, auth_url=auth_url)
  1307. else:
  1308. nova = novaclient.Client(2, user, password, auth_url=auth_url,
  1309. project_name=project)
  1310. _configure_ssh_keys(nova)
  1311. _delete_default_flavors(nova)
  1312. _ensure_flavor(nova, 'baremetal')
  1313. _ensure_flavor(nova, 'control', 'control')
  1314. _ensure_flavor(nova, 'compute', 'compute')
  1315. _ensure_flavor(nova, 'ceph-storage', 'ceph-storage')
  1316. _ensure_flavor(nova, 'block-storage', 'block-storage')
  1317. _ensure_flavor(nova, 'swift-storage', 'swift-storage')
  1318. mistral_url = instack_env['UNDERCLOUD_ENDPOINT_MISTRAL_PUBLIC']
  1319. mistral = mistralclient.client(
  1320. mistral_url=mistral_url,
  1321. session=sess)
  1322. swift = swiftclient.Connection(
  1323. authurl=auth_url,
  1324. session=sess
  1325. )
  1326. _post_config_mistral(instack_env, mistral, swift)
  1327. _member_role_exists()
  1328. def _handle_upgrade_fact(upgrade=False):
  1329. """Create an upgrade fact for use in puppet
  1330. Since we don't run different puppets depending on if it's an upgrade or
  1331. not, we need to be able to pass a flag into puppet to let it know if
  1332. we're doing an upgrade. This is helpful when trying to handle state
  1333. transitions from an already installed undercloud. This function creates
  1334. a static fact named undercloud_upgrade only after the install has occurred.
  1335. When invoked with upgrade=True, the $::undercloud_upgrade fact should
  1336. be set to true.
  1337. :param upgrade: Boolean indicating if this is an upgrade action or not
  1338. """
  1339. fact_string = 'undercloud_upgrade={}'.format(upgrade)
  1340. fact_path = '/etc/facter/facts.d/undercloud_upgrade.txt'
  1341. if not os.path.exists(os.path.dirname(fact_path)) and upgrade:
  1342. _run_command(['sudo', 'mkdir', '-p', os.path.dirname(fact_path)])
  1343. # We only need to ensure the fact is correct when we've already installed
  1344. # the undercloud.
  1345. if os.path.exists(os.path.dirname(fact_path)):
  1346. tmp_fact = tempfile.mkstemp()[1]
  1347. with open(tmp_fact, 'w') as f:
  1348. f.write(fact_string.lower())
  1349. _run_command(['sudo', 'mv', tmp_fact, fact_path])
  1350. _run_command(['sudo', 'chmod', '0644', fact_path])
  1351. def _die_tuskar_die():
  1352. """Remove tuskar* packages
  1353. Make sure to remove tuskar https://bugs.launchpad.net/tripleo/+bug/1691744
  1354. # openstack-[tuskar, tuskar-ui, tuskar-ui-extras] & python-tuskarclient
  1355. """
  1356. try:
  1357. _run_command(['sudo', 'yum', 'remove', '-y', '*tuskar*'])
  1358. except subprocess.CalledProcessError as e:
  1359. LOG.error('Error with tuskar removal task %s - continuing', e.output)
  1360. def install(instack_root, upgrade=False):
  1361. """Install the undercloud
  1362. :param instack_root: The path containing the instack-undercloud elements
  1363. and json files.
  1364. """
  1365. undercloud_operation = "upgrade" if upgrade else "install"
  1366. try:
  1367. _configure_logging(DEFAULT_LOG_LEVEL, PATHS.LOG_FILE)
  1368. LOG.info('Logging to %s', PATHS.LOG_FILE)
  1369. _load_config()
  1370. _clean_os_refresh_config()
  1371. _clean_os_collect_config()
  1372. _validate_configuration()
  1373. instack_env = _generate_environment(instack_root)
  1374. _generate_init_data(instack_env)
  1375. if upgrade:
  1376. # We didn't complete the M->N upgrades correctly with a
  1377. # `nova-manage db online_data_migrations` command before. This
  1378. # could cause the post-upgrade db sync to fail. Better be safe
  1379. # than sorry and run it before package upgrade.
  1380. _run_command(['sudo', '/usr/bin/nova-manage', 'db',
  1381. 'online_data_migrations'])
  1382. # Even if we backport https://review.openstack.org/#/c/457478/
  1383. # into stable branches of puppet-ironic, we still need a way
  1384. # to handle existing deployments.
  1385. # This task will fix ironic-dbsync.log ownership on existing
  1386. # deployments during an upgrade. It can be removed after we
  1387. # release Pike.
  1388. _run_command(['sudo', '/usr/bin/chown', 'ironic:ironic',
  1389. '/var/log/ironic/ironic-dbsync.log'])
  1390. _die_tuskar_die()
  1391. if CONF.undercloud_update_packages:
  1392. _run_yum_clean_all(instack_env)
  1393. _run_yum_update(instack_env)
  1394. _handle_upgrade_fact(upgrade)
  1395. _run_instack(instack_env)
  1396. _run_orc(instack_env)
  1397. _post_config(instack_env)
  1398. _run_command(['sudo', 'rm', '-f', '/tmp/svc-map-services'], None, 'rm')
  1399. except Exception as e:
  1400. LOG.debug("An exception occurred", exc_info=True)
  1401. LOG.error(FAILURE_MESSAGE,
  1402. {'undercloud_operation': undercloud_operation,
  1403. 'exception': six.text_type(e),
  1404. 'log_file': PATHS.LOG_FILE})
  1405. if CONF.undercloud_debug:
  1406. raise
  1407. sys.exit(1)
  1408. else:
  1409. LOG.info(COMPLETION_MESSAGE,
  1410. {'undercloud_operation': undercloud_operation,
  1411. 'password_path': PATHS.PASSWORD_PATH,
  1412. 'stackrc_path': os.path.expanduser('~/stackrc')})