oc_storageclass.py 60 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800
  1. #!/usr/bin/env python
  2. # pylint: disable=missing-docstring
  3. # flake8: noqa: T001
  4. # ___ ___ _ _ ___ ___ _ _____ ___ ___
  5. # / __| __| \| | __| _ \ /_\_ _| __| \
  6. # | (_ | _|| .` | _|| / / _ \| | | _|| |) |
  7. # \___|___|_|\_|___|_|_\/_/_\_\_|_|___|___/_ _____
  8. # | \ / _ \ | \| |/ _ \_ _| | __| \_ _|_ _|
  9. # | |) | (_) | | .` | (_) || | | _|| |) | | | |
  10. # |___/ \___/ |_|\_|\___/ |_| |___|___/___| |_|
  11. #
  12. # Copyright 2016 Red Hat, Inc. and/or its affiliates
  13. # and other contributors as indicated by the @author tags.
  14. #
  15. # Licensed under the Apache License, Version 2.0 (the "License");
  16. # you may not use this file except in compliance with the License.
  17. # You may obtain a copy of the License at
  18. #
  19. # http://www.apache.org/licenses/LICENSE-2.0
  20. #
  21. # Unless required by applicable law or agreed to in writing, software
  22. # distributed under the License is distributed on an "AS IS" BASIS,
  23. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  24. # See the License for the specific language governing permissions and
  25. # limitations under the License.
  26. #
  27. # -*- -*- -*- Begin included fragment: lib/import.py -*- -*- -*-
  28. '''
  29. OpenShiftCLI class that wraps the oc commands in a subprocess
  30. '''
  31. # pylint: disable=too-many-lines
  32. from __future__ import print_function
  33. import atexit
  34. import copy
  35. import fcntl
  36. import json
  37. import time
  38. import os
  39. import re
  40. import shutil
  41. import subprocess
  42. import tempfile
  43. # pylint: disable=import-error
  44. try:
  45. import ruamel.yaml as yaml
  46. except ImportError:
  47. import yaml
  48. from ansible.module_utils.basic import AnsibleModule
  49. # -*- -*- -*- End included fragment: lib/import.py -*- -*- -*-
  50. # -*- -*- -*- Begin included fragment: doc/storageclass -*- -*- -*-
  51. DOCUMENTATION = '''
  52. ---
  53. module: oc_storageclass
  54. short_description: Create, modify, and idempotently manage openshift storageclasses.
  55. description:
  56. - Manage openshift storageclass objects programmatically.
  57. options:
  58. state:
  59. description:
  60. - State controls the action that will be taken with resource
  61. - present - create the storageclass
  62. - absent - remove the storageclass
  63. - list - return the current representation of a storageclass
  64. required: False
  65. default: present
  66. choices: ["present", "absent", "list"]
  67. aliases: []
  68. kubeconfig:
  69. description:
  70. - The path for the kubeconfig file to use for authentication
  71. required: false
  72. default: /etc/origin/master/admin.kubeconfig
  73. aliases: []
  74. debug:
  75. description:
  76. - Turn on debug output.
  77. required: false
  78. default: False
  79. aliases: []
  80. name:
  81. description:
  82. - Name of the object that is being queried.
  83. required: false
  84. default: None
  85. aliases: []
  86. provisioner:
  87. description:
  88. - Any annotations to add to the storageclass
  89. required: false
  90. default: 'aws-ebs'
  91. aliases: []
  92. default_storage_class:
  93. description:
  94. - Whether or not this is the default storage class
  95. required: false
  96. default: False
  97. aliases: []
  98. parameters:
  99. description:
  100. - A dictionary with the parameters to configure the storageclass. This will be based on provisioner
  101. required: false
  102. default: None
  103. aliases: []
  104. api_version:
  105. description:
  106. - The api version.
  107. required: false
  108. default: v1
  109. aliases: []
  110. mount_options:
  111. description:
  112. - A list of mount options to pass when mounting volumes of this storage class.
  113. required: false
  114. default: None
  115. aliases: []
  116. reclaim_policy:
  117. description:
  118. - The reclaim policy to use for this storage class.
  119. required: false
  120. default: None
  121. aliases: []
  122. author:
  123. - "Kenny Woodson <kwoodson@redhat.com>"
  124. extends_documentation_fragment: []
  125. '''
  126. EXAMPLES = '''
  127. - name: get storageclass
  128. run_once: true
  129. oc_storageclass:
  130. name: gp2
  131. state: list
  132. register: registry_sc_out
  133. - name: create the storageclass
  134. oc_storageclass:
  135. run_once: true
  136. name: gp2
  137. parameters:
  138. type: gp2
  139. encrypted: 'true'
  140. kmsKeyId: '<full kms key arn>'
  141. provisioner: aws-ebs
  142. default_storage_class: False
  143. register: sc_out
  144. notify:
  145. - restart openshift master services
  146. '''
  147. # -*- -*- -*- End included fragment: doc/storageclass -*- -*- -*-
  148. # -*- -*- -*- Begin included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  149. class YeditException(Exception): # pragma: no cover
  150. ''' Exception class for Yedit '''
  151. pass
  152. # pylint: disable=too-many-public-methods,too-many-instance-attributes
  153. class Yedit(object): # pragma: no cover
  154. ''' Class to modify yaml files '''
  155. re_valid_key = r"(((\[-?\d+\])|([0-9a-zA-Z%s/_-]+)).?)+$"
  156. re_key = r"(?:\[(-?\d+)\])|([0-9a-zA-Z{}/_-]+)"
  157. com_sep = set(['.', '#', '|', ':'])
  158. # pylint: disable=too-many-arguments
  159. def __init__(self,
  160. filename=None,
  161. content=None,
  162. content_type='yaml',
  163. separator='.',
  164. backup_ext=None,
  165. backup=False):
  166. self.content = content
  167. self._separator = separator
  168. self.filename = filename
  169. self.__yaml_dict = content
  170. self.content_type = content_type
  171. self.backup = backup
  172. if backup_ext is None:
  173. self.backup_ext = ".{}".format(time.strftime("%Y%m%dT%H%M%S"))
  174. else:
  175. self.backup_ext = backup_ext
  176. self.load(content_type=self.content_type)
  177. if self.__yaml_dict is None:
  178. self.__yaml_dict = {}
  179. @property
  180. def separator(self):
  181. ''' getter method for separator '''
  182. return self._separator
  183. @separator.setter
  184. def separator(self, inc_sep):
  185. ''' setter method for separator '''
  186. self._separator = inc_sep
  187. @property
  188. def yaml_dict(self):
  189. ''' getter method for yaml_dict '''
  190. return self.__yaml_dict
  191. @yaml_dict.setter
  192. def yaml_dict(self, value):
  193. ''' setter method for yaml_dict '''
  194. self.__yaml_dict = value
  195. @staticmethod
  196. def parse_key(key, sep='.'):
  197. '''parse the key allowing the appropriate separator'''
  198. common_separators = list(Yedit.com_sep - set([sep]))
  199. return re.findall(Yedit.re_key.format(''.join(common_separators)), key)
  200. @staticmethod
  201. def valid_key(key, sep='.'):
  202. '''validate the incoming key'''
  203. common_separators = list(Yedit.com_sep - set([sep]))
  204. if not re.match(Yedit.re_valid_key.format(''.join(common_separators)), key):
  205. return False
  206. return True
  207. # pylint: disable=too-many-return-statements,too-many-branches
  208. @staticmethod
  209. def remove_entry(data, key, index=None, value=None, sep='.'):
  210. ''' remove data at location key '''
  211. if key == '' and isinstance(data, dict):
  212. if value is not None:
  213. data.pop(value)
  214. elif index is not None:
  215. raise YeditException("remove_entry for a dictionary does not have an index {}".format(index))
  216. else:
  217. data.clear()
  218. return True
  219. elif key == '' and isinstance(data, list):
  220. ind = None
  221. if value is not None:
  222. try:
  223. ind = data.index(value)
  224. except ValueError:
  225. return False
  226. elif index is not None:
  227. ind = index
  228. else:
  229. del data[:]
  230. if ind is not None:
  231. data.pop(ind)
  232. return True
  233. if not (key and Yedit.valid_key(key, sep)) and \
  234. isinstance(data, (list, dict)):
  235. return None
  236. key_indexes = Yedit.parse_key(key, sep)
  237. for arr_ind, dict_key in key_indexes[:-1]:
  238. if dict_key and isinstance(data, dict):
  239. data = data.get(dict_key)
  240. elif (arr_ind and isinstance(data, list) and
  241. int(arr_ind) <= len(data) - 1):
  242. data = data[int(arr_ind)]
  243. else:
  244. return None
  245. # process last index for remove
  246. # expected list entry
  247. if key_indexes[-1][0]:
  248. if isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  249. del data[int(key_indexes[-1][0])]
  250. return True
  251. # expected dict entry
  252. elif key_indexes[-1][1]:
  253. if isinstance(data, dict):
  254. del data[key_indexes[-1][1]]
  255. return True
  256. @staticmethod
  257. def add_entry(data, key, item=None, sep='.'):
  258. ''' Get an item from a dictionary with key notation a.b.c
  259. d = {'a': {'b': 'c'}}}
  260. key = a#b
  261. return c
  262. '''
  263. if key == '':
  264. pass
  265. elif (not (key and Yedit.valid_key(key, sep)) and
  266. isinstance(data, (list, dict))):
  267. return None
  268. key_indexes = Yedit.parse_key(key, sep)
  269. for arr_ind, dict_key in key_indexes[:-1]:
  270. if dict_key:
  271. if isinstance(data, dict) and dict_key in data and data[dict_key]: # noqa: E501
  272. data = data[dict_key]
  273. continue
  274. elif data and not isinstance(data, dict):
  275. raise YeditException("Unexpected item type found while going through key " +
  276. "path: {} (at key: {})".format(key, dict_key))
  277. data[dict_key] = {}
  278. data = data[dict_key]
  279. elif (arr_ind and isinstance(data, list) and
  280. int(arr_ind) <= len(data) - 1):
  281. data = data[int(arr_ind)]
  282. else:
  283. raise YeditException("Unexpected item type found while going through key path: {}".format(key))
  284. if key == '':
  285. data = item
  286. # process last index for add
  287. # expected list entry
  288. elif key_indexes[-1][0] and isinstance(data, list) and int(key_indexes[-1][0]) <= len(data) - 1: # noqa: E501
  289. data[int(key_indexes[-1][0])] = item
  290. # expected dict entry
  291. elif key_indexes[-1][1] and isinstance(data, dict):
  292. data[key_indexes[-1][1]] = item
  293. # didn't add/update to an existing list, nor add/update key to a dict
  294. # so we must have been provided some syntax like a.b.c[<int>] = "data" for a
  295. # non-existent array
  296. else:
  297. raise YeditException("Error adding to object at path: {}".format(key))
  298. return data
  299. @staticmethod
  300. def get_entry(data, key, sep='.'):
  301. ''' Get an item from a dictionary with key notation a.b.c
  302. d = {'a': {'b': 'c'}}}
  303. key = a.b
  304. return c
  305. '''
  306. if key == '':
  307. pass
  308. elif (not (key and Yedit.valid_key(key, sep)) and
  309. isinstance(data, (list, dict))):
  310. return None
  311. key_indexes = Yedit.parse_key(key, sep)
  312. for arr_ind, dict_key in key_indexes:
  313. if dict_key and isinstance(data, dict):
  314. data = data.get(dict_key)
  315. elif (arr_ind and isinstance(data, list) and
  316. int(arr_ind) <= len(data) - 1):
  317. data = data[int(arr_ind)]
  318. else:
  319. return None
  320. return data
  321. @staticmethod
  322. def _write(filename, contents):
  323. ''' Actually write the file contents to disk. This helps with mocking. '''
  324. tmp_filename = filename + '.yedit'
  325. with open(tmp_filename, 'w') as yfd:
  326. fcntl.flock(yfd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  327. yfd.write(contents)
  328. fcntl.flock(yfd, fcntl.LOCK_UN)
  329. os.rename(tmp_filename, filename)
  330. def write(self):
  331. ''' write to file '''
  332. if not self.filename:
  333. raise YeditException('Please specify a filename.')
  334. if self.backup and self.file_exists():
  335. shutil.copy(self.filename, '{}{}'.format(self.filename, self.backup_ext))
  336. # Try to set format attributes if supported
  337. try:
  338. self.yaml_dict.fa.set_block_style()
  339. except AttributeError:
  340. pass
  341. # Try to use RoundTripDumper if supported.
  342. if self.content_type == 'yaml':
  343. try:
  344. Yedit._write(self.filename, yaml.dump(self.yaml_dict, Dumper=yaml.RoundTripDumper))
  345. except AttributeError:
  346. Yedit._write(self.filename, yaml.safe_dump(self.yaml_dict, default_flow_style=False))
  347. elif self.content_type == 'json':
  348. Yedit._write(self.filename, json.dumps(self.yaml_dict, indent=4, sort_keys=True))
  349. else:
  350. raise YeditException('Unsupported content_type: {}.'.format(self.content_type) +
  351. 'Please specify a content_type of yaml or json.')
  352. return (True, self.yaml_dict)
  353. def read(self):
  354. ''' read from file '''
  355. # check if it exists
  356. if self.filename is None or not self.file_exists():
  357. return None
  358. contents = None
  359. with open(self.filename) as yfd:
  360. contents = yfd.read()
  361. return contents
  362. def file_exists(self):
  363. ''' return whether file exists '''
  364. if os.path.exists(self.filename):
  365. return True
  366. return False
  367. def load(self, content_type='yaml'):
  368. ''' return yaml file '''
  369. contents = self.read()
  370. if not contents and not self.content:
  371. return None
  372. if self.content:
  373. if isinstance(self.content, dict):
  374. self.yaml_dict = self.content
  375. return self.yaml_dict
  376. elif isinstance(self.content, str):
  377. contents = self.content
  378. # check if it is yaml
  379. try:
  380. if content_type == 'yaml' and contents:
  381. # Try to set format attributes if supported
  382. try:
  383. self.yaml_dict.fa.set_block_style()
  384. except AttributeError:
  385. pass
  386. # Try to use RoundTripLoader if supported.
  387. try:
  388. self.yaml_dict = yaml.load(contents, yaml.RoundTripLoader)
  389. except AttributeError:
  390. self.yaml_dict = yaml.safe_load(contents)
  391. # Try to set format attributes if supported
  392. try:
  393. self.yaml_dict.fa.set_block_style()
  394. except AttributeError:
  395. pass
  396. elif content_type == 'json' and contents:
  397. self.yaml_dict = json.loads(contents)
  398. except yaml.YAMLError as err:
  399. # Error loading yaml or json
  400. raise YeditException('Problem with loading yaml file. {}'.format(err))
  401. return self.yaml_dict
  402. def get(self, key):
  403. ''' get a specified key'''
  404. try:
  405. entry = Yedit.get_entry(self.yaml_dict, key, self.separator)
  406. except KeyError:
  407. entry = None
  408. return entry
  409. def pop(self, path, key_or_item):
  410. ''' remove a key, value pair from a dict or an item for a list'''
  411. try:
  412. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  413. except KeyError:
  414. entry = None
  415. if entry is None:
  416. return (False, self.yaml_dict)
  417. if isinstance(entry, dict):
  418. # AUDIT:maybe-no-member makes sense due to fuzzy types
  419. # pylint: disable=maybe-no-member
  420. if key_or_item in entry:
  421. entry.pop(key_or_item)
  422. return (True, self.yaml_dict)
  423. return (False, self.yaml_dict)
  424. elif isinstance(entry, list):
  425. # AUDIT:maybe-no-member makes sense due to fuzzy types
  426. # pylint: disable=maybe-no-member
  427. ind = None
  428. try:
  429. ind = entry.index(key_or_item)
  430. except ValueError:
  431. return (False, self.yaml_dict)
  432. entry.pop(ind)
  433. return (True, self.yaml_dict)
  434. return (False, self.yaml_dict)
  435. def delete(self, path, index=None, value=None):
  436. ''' remove path from a dict'''
  437. try:
  438. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  439. except KeyError:
  440. entry = None
  441. if entry is None:
  442. return (False, self.yaml_dict)
  443. result = Yedit.remove_entry(self.yaml_dict, path, index, value, self.separator)
  444. if not result:
  445. return (False, self.yaml_dict)
  446. return (True, self.yaml_dict)
  447. def exists(self, path, value):
  448. ''' check if value exists at path'''
  449. try:
  450. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  451. except KeyError:
  452. entry = None
  453. if isinstance(entry, list):
  454. if value in entry:
  455. return True
  456. return False
  457. elif isinstance(entry, dict):
  458. if isinstance(value, dict):
  459. rval = False
  460. for key, val in value.items():
  461. if entry[key] != val:
  462. rval = False
  463. break
  464. else:
  465. rval = True
  466. return rval
  467. return value in entry
  468. return entry == value
  469. def append(self, path, value):
  470. '''append value to a list'''
  471. try:
  472. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  473. except KeyError:
  474. entry = None
  475. if entry is None:
  476. self.put(path, [])
  477. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  478. if not isinstance(entry, list):
  479. return (False, self.yaml_dict)
  480. # AUDIT:maybe-no-member makes sense due to loading data from
  481. # a serialized format.
  482. # pylint: disable=maybe-no-member
  483. entry.append(value)
  484. return (True, self.yaml_dict)
  485. # pylint: disable=too-many-arguments
  486. def update(self, path, value, index=None, curr_value=None):
  487. ''' put path, value into a dict '''
  488. try:
  489. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  490. except KeyError:
  491. entry = None
  492. if isinstance(entry, dict):
  493. # AUDIT:maybe-no-member makes sense due to fuzzy types
  494. # pylint: disable=maybe-no-member
  495. if not isinstance(value, dict):
  496. raise YeditException('Cannot replace key, value entry in dict with non-dict type. ' +
  497. 'value=[{}] type=[{}]'.format(value, type(value)))
  498. entry.update(value)
  499. return (True, self.yaml_dict)
  500. elif isinstance(entry, list):
  501. # AUDIT:maybe-no-member makes sense due to fuzzy types
  502. # pylint: disable=maybe-no-member
  503. ind = None
  504. if curr_value:
  505. try:
  506. ind = entry.index(curr_value)
  507. except ValueError:
  508. return (False, self.yaml_dict)
  509. elif index is not None:
  510. ind = index
  511. if ind is not None and entry[ind] != value:
  512. entry[ind] = value
  513. return (True, self.yaml_dict)
  514. # see if it exists in the list
  515. try:
  516. ind = entry.index(value)
  517. except ValueError:
  518. # doesn't exist, append it
  519. entry.append(value)
  520. return (True, self.yaml_dict)
  521. # already exists, return
  522. if ind is not None:
  523. return (False, self.yaml_dict)
  524. return (False, self.yaml_dict)
  525. def put(self, path, value):
  526. ''' put path, value into a dict '''
  527. try:
  528. entry = Yedit.get_entry(self.yaml_dict, path, self.separator)
  529. except KeyError:
  530. entry = None
  531. if entry == value:
  532. return (False, self.yaml_dict)
  533. # deepcopy didn't work
  534. # Try to use ruamel.yaml and fallback to pyyaml
  535. try:
  536. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  537. default_flow_style=False),
  538. yaml.RoundTripLoader)
  539. except AttributeError:
  540. tmp_copy = copy.deepcopy(self.yaml_dict)
  541. # set the format attributes if available
  542. try:
  543. tmp_copy.fa.set_block_style()
  544. except AttributeError:
  545. pass
  546. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  547. if result is None:
  548. return (False, self.yaml_dict)
  549. # When path equals "" it is a special case.
  550. # "" refers to the root of the document
  551. # Only update the root path (entire document) when its a list or dict
  552. if path == '':
  553. if isinstance(result, list) or isinstance(result, dict):
  554. self.yaml_dict = result
  555. return (True, self.yaml_dict)
  556. return (False, self.yaml_dict)
  557. self.yaml_dict = tmp_copy
  558. return (True, self.yaml_dict)
  559. def create(self, path, value):
  560. ''' create a yaml file '''
  561. if not self.file_exists():
  562. # deepcopy didn't work
  563. # Try to use ruamel.yaml and fallback to pyyaml
  564. try:
  565. tmp_copy = yaml.load(yaml.round_trip_dump(self.yaml_dict,
  566. default_flow_style=False),
  567. yaml.RoundTripLoader)
  568. except AttributeError:
  569. tmp_copy = copy.deepcopy(self.yaml_dict)
  570. # set the format attributes if available
  571. try:
  572. tmp_copy.fa.set_block_style()
  573. except AttributeError:
  574. pass
  575. result = Yedit.add_entry(tmp_copy, path, value, self.separator)
  576. if result is not None:
  577. self.yaml_dict = tmp_copy
  578. return (True, self.yaml_dict)
  579. return (False, self.yaml_dict)
  580. @staticmethod
  581. def get_curr_value(invalue, val_type):
  582. '''return the current value'''
  583. if invalue is None:
  584. return None
  585. curr_value = invalue
  586. if val_type == 'yaml':
  587. curr_value = yaml.safe_load(str(invalue))
  588. elif val_type == 'json':
  589. curr_value = json.loads(invalue)
  590. return curr_value
  591. @staticmethod
  592. def parse_value(inc_value, vtype=''):
  593. '''determine value type passed'''
  594. true_bools = ['y', 'Y', 'yes', 'Yes', 'YES', 'true', 'True', 'TRUE',
  595. 'on', 'On', 'ON', ]
  596. false_bools = ['n', 'N', 'no', 'No', 'NO', 'false', 'False', 'FALSE',
  597. 'off', 'Off', 'OFF']
  598. # It came in as a string but you didn't specify value_type as string
  599. # we will convert to bool if it matches any of the above cases
  600. if isinstance(inc_value, str) and 'bool' in vtype:
  601. if inc_value not in true_bools and inc_value not in false_bools:
  602. raise YeditException('Not a boolean type. str=[{}] vtype=[{}]'.format(inc_value, vtype))
  603. elif isinstance(inc_value, bool) and 'str' in vtype:
  604. inc_value = str(inc_value)
  605. # There is a special case where '' will turn into None after yaml loading it so skip
  606. if isinstance(inc_value, str) and inc_value == '':
  607. pass
  608. # If vtype is not str then go ahead and attempt to yaml load it.
  609. elif isinstance(inc_value, str) and 'str' not in vtype:
  610. try:
  611. inc_value = yaml.safe_load(inc_value)
  612. except Exception:
  613. raise YeditException('Could not determine type of incoming value. ' +
  614. 'value=[{}] vtype=[{}]'.format(type(inc_value), vtype))
  615. return inc_value
  616. @staticmethod
  617. def process_edits(edits, yamlfile):
  618. '''run through a list of edits and process them one-by-one'''
  619. results = []
  620. for edit in edits:
  621. value = Yedit.parse_value(edit['value'], edit.get('value_type', ''))
  622. if edit.get('action') == 'update':
  623. # pylint: disable=line-too-long
  624. curr_value = Yedit.get_curr_value(
  625. Yedit.parse_value(edit.get('curr_value')),
  626. edit.get('curr_value_format'))
  627. rval = yamlfile.update(edit['key'],
  628. value,
  629. edit.get('index'),
  630. curr_value)
  631. elif edit.get('action') == 'append':
  632. rval = yamlfile.append(edit['key'], value)
  633. else:
  634. rval = yamlfile.put(edit['key'], value)
  635. if rval[0]:
  636. results.append({'key': edit['key'], 'edit': rval[1]})
  637. return {'changed': len(results) > 0, 'results': results}
  638. # pylint: disable=too-many-return-statements,too-many-branches
  639. @staticmethod
  640. def run_ansible(params):
  641. '''perform the idempotent crud operations'''
  642. yamlfile = Yedit(filename=params['src'],
  643. backup=params['backup'],
  644. content_type=params['content_type'],
  645. backup_ext=params['backup_ext'],
  646. separator=params['separator'])
  647. state = params['state']
  648. if params['src']:
  649. rval = yamlfile.load()
  650. if yamlfile.yaml_dict is None and state != 'present':
  651. return {'failed': True,
  652. 'msg': 'Error opening file [{}]. Verify that the '.format(params['src']) +
  653. 'file exists, that it is has correct permissions, and is valid yaml.'}
  654. if state == 'list':
  655. if params['content']:
  656. content = Yedit.parse_value(params['content'], params['content_type'])
  657. yamlfile.yaml_dict = content
  658. if params['key']:
  659. rval = yamlfile.get(params['key'])
  660. return {'changed': False, 'result': rval, 'state': state}
  661. elif state == 'absent':
  662. if params['content']:
  663. content = Yedit.parse_value(params['content'], params['content_type'])
  664. yamlfile.yaml_dict = content
  665. if params['update']:
  666. rval = yamlfile.pop(params['key'], params['value'])
  667. else:
  668. rval = yamlfile.delete(params['key'], params['index'], params['value'])
  669. if rval[0] and params['src']:
  670. yamlfile.write()
  671. return {'changed': rval[0], 'result': rval[1], 'state': state}
  672. elif state == 'present':
  673. # check if content is different than what is in the file
  674. if params['content']:
  675. content = Yedit.parse_value(params['content'], params['content_type'])
  676. # We had no edits to make and the contents are the same
  677. if yamlfile.yaml_dict == content and \
  678. params['value'] is None:
  679. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  680. yamlfile.yaml_dict = content
  681. # If we were passed a key, value then
  682. # we enapsulate it in a list and process it
  683. # Key, Value passed to the module : Converted to Edits list #
  684. edits = []
  685. _edit = {}
  686. if params['value'] is not None:
  687. _edit['value'] = params['value']
  688. _edit['value_type'] = params['value_type']
  689. _edit['key'] = params['key']
  690. if params['update']:
  691. _edit['action'] = 'update'
  692. _edit['curr_value'] = params['curr_value']
  693. _edit['curr_value_format'] = params['curr_value_format']
  694. _edit['index'] = params['index']
  695. elif params['append']:
  696. _edit['action'] = 'append'
  697. edits.append(_edit)
  698. elif params['edits'] is not None:
  699. edits = params['edits']
  700. if edits:
  701. results = Yedit.process_edits(edits, yamlfile)
  702. # if there were changes and a src provided to us we need to write
  703. if results['changed'] and params['src']:
  704. yamlfile.write()
  705. return {'changed': results['changed'], 'result': results['results'], 'state': state}
  706. # no edits to make
  707. if params['src']:
  708. # pylint: disable=redefined-variable-type
  709. rval = yamlfile.write()
  710. return {'changed': rval[0],
  711. 'result': rval[1],
  712. 'state': state}
  713. # We were passed content but no src, key or value, or edits. Return contents in memory
  714. return {'changed': False, 'result': yamlfile.yaml_dict, 'state': state}
  715. return {'failed': True, 'msg': 'Unkown state passed'}
  716. # -*- -*- -*- End included fragment: ../../lib_utils/src/class/yedit.py -*- -*- -*-
  717. # -*- -*- -*- Begin included fragment: lib/base.py -*- -*- -*-
  718. # pylint: disable=too-many-lines
  719. # noqa: E301,E302,E303,T001
  720. class OpenShiftCLIError(Exception):
  721. '''Exception class for openshiftcli'''
  722. pass
  723. ADDITIONAL_PATH_LOOKUPS = ['/usr/local/bin', os.path.expanduser('~/bin')]
  724. def locate_oc_binary():
  725. ''' Find and return oc binary file '''
  726. # https://github.com/openshift/openshift-ansible/issues/3410
  727. # oc can be in /usr/local/bin in some cases, but that may not
  728. # be in $PATH due to ansible/sudo
  729. paths = os.environ.get("PATH", os.defpath).split(os.pathsep) + ADDITIONAL_PATH_LOOKUPS
  730. oc_binary = 'oc'
  731. # Use shutil.which if it is available, otherwise fallback to a naive path search
  732. try:
  733. which_result = shutil.which(oc_binary, path=os.pathsep.join(paths))
  734. if which_result is not None:
  735. oc_binary = which_result
  736. except AttributeError:
  737. for path in paths:
  738. if os.path.exists(os.path.join(path, oc_binary)):
  739. oc_binary = os.path.join(path, oc_binary)
  740. break
  741. return oc_binary
  742. # pylint: disable=too-few-public-methods
  743. class OpenShiftCLI(object):
  744. ''' Class to wrap the command line tools '''
  745. def __init__(self,
  746. namespace,
  747. kubeconfig='/etc/origin/master/admin.kubeconfig',
  748. verbose=False,
  749. all_namespaces=False):
  750. ''' Constructor for OpenshiftCLI '''
  751. self.namespace = namespace
  752. self.verbose = verbose
  753. self.kubeconfig = Utils.create_tmpfile_copy(kubeconfig)
  754. self.all_namespaces = all_namespaces
  755. self.oc_binary = locate_oc_binary()
  756. # Pylint allows only 5 arguments to be passed.
  757. # pylint: disable=too-many-arguments
  758. def _replace_content(self, resource, rname, content, edits=None, force=False, sep='.'):
  759. ''' replace the current object with the content '''
  760. res = self._get(resource, rname)
  761. if not res['results']:
  762. return res
  763. fname = Utils.create_tmpfile(rname + '-')
  764. yed = Yedit(fname, res['results'][0], separator=sep)
  765. updated = False
  766. if content is not None:
  767. changes = []
  768. for key, value in content.items():
  769. changes.append(yed.put(key, value))
  770. if any([change[0] for change in changes]):
  771. updated = True
  772. elif edits is not None:
  773. results = Yedit.process_edits(edits, yed)
  774. if results['changed']:
  775. updated = True
  776. if updated:
  777. yed.write()
  778. atexit.register(Utils.cleanup, [fname])
  779. return self._replace(fname, force)
  780. return {'returncode': 0, 'updated': False}
  781. def _replace(self, fname, force=False):
  782. '''replace the current object with oc replace'''
  783. # We are removing the 'resourceVersion' to handle
  784. # a race condition when modifying oc objects
  785. yed = Yedit(fname)
  786. results = yed.delete('metadata.resourceVersion')
  787. if results[0]:
  788. yed.write()
  789. cmd = ['replace', '-f', fname]
  790. if force:
  791. cmd.append('--force')
  792. return self.openshift_cmd(cmd)
  793. def _create_from_content(self, rname, content):
  794. '''create a temporary file and then call oc create on it'''
  795. fname = Utils.create_tmpfile(rname + '-')
  796. yed = Yedit(fname, content=content)
  797. yed.write()
  798. atexit.register(Utils.cleanup, [fname])
  799. return self._create(fname)
  800. def _create(self, fname):
  801. '''call oc create on a filename'''
  802. return self.openshift_cmd(['create', '-f', fname])
  803. def _delete(self, resource, name=None, selector=None):
  804. '''call oc delete on a resource'''
  805. cmd = ['delete', resource]
  806. if selector is not None:
  807. cmd.append('--selector={}'.format(selector))
  808. elif name is not None:
  809. cmd.append(name)
  810. else:
  811. raise OpenShiftCLIError('Either name or selector is required when calling delete.')
  812. return self.openshift_cmd(cmd)
  813. def _process(self, template_name, create=False, params=None, template_data=None): # noqa: E501
  814. '''process a template
  815. template_name: the name of the template to process
  816. create: whether to send to oc create after processing
  817. params: the parameters for the template
  818. template_data: the incoming template's data; instead of a file
  819. '''
  820. cmd = ['process']
  821. if template_data:
  822. cmd.extend(['-f', '-'])
  823. else:
  824. cmd.append(template_name)
  825. if params:
  826. param_str = ["{}={}".format(key, str(value).replace("'", r'"')) for key, value in params.items()]
  827. cmd.append('-p')
  828. cmd.extend(param_str)
  829. results = self.openshift_cmd(cmd, output=True, input_data=template_data)
  830. if results['returncode'] != 0 or not create:
  831. return results
  832. fname = Utils.create_tmpfile(template_name + '-')
  833. yed = Yedit(fname, results['results'])
  834. yed.write()
  835. atexit.register(Utils.cleanup, [fname])
  836. return self.openshift_cmd(['create', '-f', fname])
  837. def _get(self, resource, name=None, selector=None, field_selector=None):
  838. '''return a resource by name '''
  839. cmd = ['get', resource]
  840. if selector is not None:
  841. cmd.append('--selector={}'.format(selector))
  842. if field_selector is not None:
  843. cmd.append('--field-selector={}'.format(field_selector))
  844. # Name cannot be used with selector or field_selector.
  845. if selector is None and field_selector is None and name is not None:
  846. cmd.append(name)
  847. cmd.extend(['-o', 'json'])
  848. rval = self.openshift_cmd(cmd, output=True)
  849. # Ensure results are retuned in an array
  850. if 'items' in rval:
  851. rval['results'] = rval['items']
  852. elif not isinstance(rval['results'], list):
  853. rval['results'] = [rval['results']]
  854. return rval
  855. def _schedulable(self, node=None, selector=None, schedulable=True):
  856. ''' perform oadm manage-node scheduable '''
  857. cmd = ['manage-node']
  858. if node:
  859. cmd.extend(node)
  860. else:
  861. cmd.append('--selector={}'.format(selector))
  862. cmd.append('--schedulable={}'.format(schedulable))
  863. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw') # noqa: E501
  864. def _list_pods(self, node=None, selector=None, pod_selector=None):
  865. ''' perform oadm list pods
  866. node: the node in which to list pods
  867. selector: the label selector filter if provided
  868. pod_selector: the pod selector filter if provided
  869. '''
  870. cmd = ['manage-node']
  871. if node:
  872. cmd.extend(node)
  873. else:
  874. cmd.append('--selector={}'.format(selector))
  875. if pod_selector:
  876. cmd.append('--pod-selector={}'.format(pod_selector))
  877. cmd.extend(['--list-pods', '-o', 'json'])
  878. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  879. # pylint: disable=too-many-arguments
  880. def _evacuate(self, node=None, selector=None, pod_selector=None, dry_run=False, grace_period=None, force=False):
  881. ''' perform oadm manage-node evacuate '''
  882. cmd = ['manage-node']
  883. if node:
  884. cmd.extend(node)
  885. else:
  886. cmd.append('--selector={}'.format(selector))
  887. if dry_run:
  888. cmd.append('--dry-run')
  889. if pod_selector:
  890. cmd.append('--pod-selector={}'.format(pod_selector))
  891. if grace_period:
  892. cmd.append('--grace-period={}'.format(int(grace_period)))
  893. if force:
  894. cmd.append('--force')
  895. cmd.append('--evacuate')
  896. return self.openshift_cmd(cmd, oadm=True, output=True, output_type='raw')
  897. def _version(self):
  898. ''' return the openshift version'''
  899. return self.openshift_cmd(['version'], output=True, output_type='raw')
  900. def _import_image(self, url=None, name=None, tag=None):
  901. ''' perform image import '''
  902. cmd = ['import-image']
  903. image = '{0}'.format(name)
  904. if tag:
  905. image += ':{0}'.format(tag)
  906. cmd.append(image)
  907. if url:
  908. cmd.append('--from={0}/{1}'.format(url, image))
  909. cmd.append('-n{0}'.format(self.namespace))
  910. cmd.append('--confirm')
  911. return self.openshift_cmd(cmd)
  912. def _run(self, cmds, input_data):
  913. ''' Actually executes the command. This makes mocking easier. '''
  914. curr_env = os.environ.copy()
  915. curr_env.update({'KUBECONFIG': self.kubeconfig})
  916. proc = subprocess.Popen(cmds,
  917. stdin=subprocess.PIPE,
  918. stdout=subprocess.PIPE,
  919. stderr=subprocess.PIPE,
  920. env=curr_env)
  921. stdout, stderr = proc.communicate(input_data)
  922. return proc.returncode, stdout.decode('utf-8'), stderr.decode('utf-8')
  923. # pylint: disable=too-many-arguments,too-many-branches
  924. def openshift_cmd(self, cmd, oadm=False, output=False, output_type='json', input_data=None):
  925. '''Base command for oc '''
  926. cmds = [self.oc_binary]
  927. if oadm:
  928. cmds.append('adm')
  929. cmds.extend(cmd)
  930. if self.all_namespaces:
  931. cmds.extend(['--all-namespaces'])
  932. elif self.namespace is not None and self.namespace.lower() not in ['none', 'emtpy']: # E501
  933. cmds.extend(['-n', self.namespace])
  934. if self.verbose:
  935. print(' '.join(cmds))
  936. try:
  937. returncode, stdout, stderr = self._run(cmds, input_data)
  938. except OSError as ex:
  939. returncode, stdout, stderr = 1, '', 'Failed to execute {}: {}'.format(subprocess.list2cmdline(cmds), ex)
  940. rval = {"returncode": returncode,
  941. "cmd": ' '.join(cmds)}
  942. if output_type == 'json':
  943. rval['results'] = {}
  944. if output and stdout:
  945. try:
  946. rval['results'] = json.loads(stdout)
  947. except ValueError as verr:
  948. if "No JSON object could be decoded" in verr.args:
  949. rval['err'] = verr.args
  950. elif output_type == 'raw':
  951. rval['results'] = stdout if output else ''
  952. if self.verbose:
  953. print("STDOUT: {0}".format(stdout))
  954. print("STDERR: {0}".format(stderr))
  955. if 'err' in rval or returncode != 0:
  956. rval.update({"stderr": stderr,
  957. "stdout": stdout})
  958. return rval
  959. class Utils(object): # pragma: no cover
  960. ''' utilities for openshiftcli modules '''
  961. @staticmethod
  962. def _write(filename, contents):
  963. ''' Actually write the file contents to disk. This helps with mocking. '''
  964. with open(filename, 'w') as sfd:
  965. sfd.write(str(contents))
  966. @staticmethod
  967. def create_tmp_file_from_contents(rname, data, ftype='yaml'):
  968. ''' create a file in tmp with name and contents'''
  969. tmp = Utils.create_tmpfile(prefix=rname)
  970. if ftype == 'yaml':
  971. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  972. # pylint: disable=no-member
  973. if hasattr(yaml, 'RoundTripDumper'):
  974. Utils._write(tmp, yaml.dump(data, Dumper=yaml.RoundTripDumper))
  975. else:
  976. Utils._write(tmp, yaml.safe_dump(data, default_flow_style=False))
  977. elif ftype == 'json':
  978. Utils._write(tmp, json.dumps(data))
  979. else:
  980. Utils._write(tmp, data)
  981. # Register cleanup when module is done
  982. atexit.register(Utils.cleanup, [tmp])
  983. return tmp
  984. @staticmethod
  985. def create_tmpfile_copy(inc_file):
  986. '''create a temporary copy of a file'''
  987. tmpfile = Utils.create_tmpfile('lib_openshift-')
  988. Utils._write(tmpfile, open(inc_file).read())
  989. # Cleanup the tmpfile
  990. atexit.register(Utils.cleanup, [tmpfile])
  991. return tmpfile
  992. @staticmethod
  993. def create_tmpfile(prefix='tmp'):
  994. ''' Generates and returns a temporary file name '''
  995. with tempfile.NamedTemporaryFile(prefix=prefix, delete=False) as tmp:
  996. return tmp.name
  997. @staticmethod
  998. def create_tmp_files_from_contents(content, content_type=None):
  999. '''Turn an array of dict: filename, content into a files array'''
  1000. if not isinstance(content, list):
  1001. content = [content]
  1002. files = []
  1003. for item in content:
  1004. path = Utils.create_tmp_file_from_contents(item['path'] + '-',
  1005. item['data'],
  1006. ftype=content_type)
  1007. files.append({'name': os.path.basename(item['path']),
  1008. 'path': path})
  1009. return files
  1010. @staticmethod
  1011. def cleanup(files):
  1012. '''Clean up on exit '''
  1013. for sfile in files:
  1014. if os.path.exists(sfile):
  1015. if os.path.isdir(sfile):
  1016. shutil.rmtree(sfile)
  1017. elif os.path.isfile(sfile):
  1018. os.remove(sfile)
  1019. @staticmethod
  1020. def exists(results, _name):
  1021. ''' Check to see if the results include the name '''
  1022. if not results:
  1023. return False
  1024. if Utils.find_result(results, _name):
  1025. return True
  1026. return False
  1027. @staticmethod
  1028. def find_result(results, _name):
  1029. ''' Find the specified result by name'''
  1030. rval = None
  1031. for result in results:
  1032. if 'metadata' in result and result['metadata']['name'] == _name:
  1033. rval = result
  1034. break
  1035. return rval
  1036. @staticmethod
  1037. def get_resource_file(sfile, sfile_type='yaml'):
  1038. ''' return the service file '''
  1039. contents = None
  1040. with open(sfile) as sfd:
  1041. contents = sfd.read()
  1042. if sfile_type == 'yaml':
  1043. # AUDIT:no-member makes sense here due to ruamel.YAML/PyYAML usage
  1044. # pylint: disable=no-member
  1045. if hasattr(yaml, 'RoundTripLoader'):
  1046. contents = yaml.load(contents, yaml.RoundTripLoader)
  1047. else:
  1048. contents = yaml.safe_load(contents)
  1049. elif sfile_type == 'json':
  1050. contents = json.loads(contents)
  1051. return contents
  1052. @staticmethod
  1053. def filter_versions(stdout):
  1054. ''' filter the oc version output '''
  1055. version_dict = {}
  1056. version_search = ['oc', 'openshift', 'kubernetes']
  1057. for line in stdout.strip().split('\n'):
  1058. for term in version_search:
  1059. if not line:
  1060. continue
  1061. if line.startswith(term):
  1062. version_dict[term] = line.split()[-1]
  1063. # horrible hack to get openshift version in Openshift 3.2
  1064. # By default "oc version in 3.2 does not return an "openshift" version
  1065. if "openshift" not in version_dict:
  1066. version_dict["openshift"] = version_dict["oc"]
  1067. return version_dict
  1068. @staticmethod
  1069. def add_custom_versions(versions):
  1070. ''' create custom versions strings '''
  1071. versions_dict = {}
  1072. for tech, version in versions.items():
  1073. # clean up "-" from version
  1074. if "-" in version:
  1075. version = version.split("-")[0]
  1076. if version.startswith('v'):
  1077. version = version[1:] # Remove the 'v' prefix
  1078. versions_dict[tech + '_numeric'] = version.split('+')[0]
  1079. # "3.3.0.33" is what we have, we want "3.3"
  1080. versions_dict[tech + '_short'] = "{}.{}".format(*version.split('.'))
  1081. return versions_dict
  1082. @staticmethod
  1083. def openshift_installed():
  1084. ''' check if openshift is installed '''
  1085. import rpm
  1086. transaction_set = rpm.TransactionSet()
  1087. rpmquery = transaction_set.dbMatch("name", "atomic-openshift")
  1088. return rpmquery.count() > 0
  1089. # Disabling too-many-branches. This is a yaml dictionary comparison function
  1090. # pylint: disable=too-many-branches,too-many-return-statements,too-many-statements
  1091. @staticmethod
  1092. def check_def_equal(user_def, result_def, skip_keys=None, debug=False):
  1093. ''' Given a user defined definition, compare it with the results given back by our query. '''
  1094. # Currently these values are autogenerated and we do not need to check them
  1095. skip = ['metadata', 'status']
  1096. if skip_keys:
  1097. skip.extend(skip_keys)
  1098. for key, value in result_def.items():
  1099. if key in skip:
  1100. continue
  1101. # Both are lists
  1102. if isinstance(value, list):
  1103. if key not in user_def:
  1104. if debug:
  1105. print('User data does not have key [%s]' % key)
  1106. print('User data: %s' % user_def)
  1107. return False
  1108. if not isinstance(user_def[key], list):
  1109. if debug:
  1110. print('user_def[key] is not a list key=[%s] user_def[key]=%s' % (key, user_def[key]))
  1111. return False
  1112. if len(user_def[key]) != len(value):
  1113. if debug:
  1114. print("List lengths are not equal.")
  1115. print("key=[%s]: user_def[%s] != value[%s]" % (key, len(user_def[key]), len(value)))
  1116. print("user_def: %s" % user_def[key])
  1117. print("value: %s" % value)
  1118. return False
  1119. for values in zip(user_def[key], value):
  1120. if isinstance(values[0], dict) and isinstance(values[1], dict):
  1121. if debug:
  1122. print('sending list - list')
  1123. print(type(values[0]))
  1124. print(type(values[1]))
  1125. result = Utils.check_def_equal(values[0], values[1], skip_keys=skip_keys, debug=debug)
  1126. if not result:
  1127. print('list compare returned false')
  1128. return False
  1129. elif value != user_def[key]:
  1130. if debug:
  1131. print('value should be identical')
  1132. print(user_def[key])
  1133. print(value)
  1134. return False
  1135. # recurse on a dictionary
  1136. elif isinstance(value, dict):
  1137. if key not in user_def:
  1138. if debug:
  1139. print("user_def does not have key [%s]" % key)
  1140. return False
  1141. if not isinstance(user_def[key], dict):
  1142. if debug:
  1143. print("dict returned false: not instance of dict")
  1144. return False
  1145. # before passing ensure keys match
  1146. api_values = set(value.keys()) - set(skip)
  1147. user_values = set(user_def[key].keys()) - set(skip)
  1148. if api_values != user_values:
  1149. if debug:
  1150. print("keys are not equal in dict")
  1151. print(user_values)
  1152. print(api_values)
  1153. return False
  1154. result = Utils.check_def_equal(user_def[key], value, skip_keys=skip_keys, debug=debug)
  1155. if not result:
  1156. if debug:
  1157. print("dict returned false")
  1158. print(result)
  1159. return False
  1160. # Verify each key, value pair is the same
  1161. else:
  1162. if key not in user_def or value != user_def[key]:
  1163. if debug:
  1164. print("value not equal; user_def does not have key")
  1165. print(key)
  1166. print(value)
  1167. if key in user_def:
  1168. print(user_def[key])
  1169. return False
  1170. if debug:
  1171. print('returning true')
  1172. return True
  1173. class OpenShiftCLIConfig(object):
  1174. '''Generic Config'''
  1175. def __init__(self, rname, namespace, kubeconfig, options):
  1176. self.kubeconfig = kubeconfig
  1177. self.name = rname
  1178. self.namespace = namespace
  1179. self._options = options
  1180. @property
  1181. def config_options(self):
  1182. ''' return config options '''
  1183. return self._options
  1184. def to_option_list(self, ascommalist=''):
  1185. '''return all options as a string
  1186. if ascommalist is set to the name of a key, and
  1187. the value of that key is a dict, format the dict
  1188. as a list of comma delimited key=value pairs'''
  1189. return self.stringify(ascommalist)
  1190. def stringify(self, ascommalist=''):
  1191. ''' return the options hash as cli params in a string
  1192. if ascommalist is set to the name of a key, and
  1193. the value of that key is a dict, format the dict
  1194. as a list of comma delimited key=value pairs '''
  1195. rval = []
  1196. for key in sorted(self.config_options.keys()):
  1197. data = self.config_options[key]
  1198. if data['include'] \
  1199. and (data['value'] is not None or isinstance(data['value'], int)):
  1200. if key == ascommalist:
  1201. val = ','.join(['{}={}'.format(kk, vv) for kk, vv in sorted(data['value'].items())])
  1202. else:
  1203. val = data['value']
  1204. rval.append('--{}={}'.format(key.replace('_', '-'), val))
  1205. return rval
  1206. # -*- -*- -*- End included fragment: lib/base.py -*- -*- -*-
  1207. # -*- -*- -*- Begin included fragment: lib/storageclass.py -*- -*- -*-
  1208. # pylint: disable=too-many-instance-attributes
  1209. class StorageClassConfig(object):
  1210. ''' Handle service options '''
  1211. # pylint: disable=too-many-arguments
  1212. def __init__(self,
  1213. name,
  1214. provisioner,
  1215. parameters=None,
  1216. annotations=None,
  1217. default_storage_class="false",
  1218. api_version='v1',
  1219. kubeconfig='/etc/origin/master/admin.kubeconfig',
  1220. mount_options=None,
  1221. reclaim_policy=None):
  1222. ''' constructor for handling storageclass options '''
  1223. self.name = name
  1224. self.parameters = parameters
  1225. self.annotations = annotations
  1226. self.provisioner = provisioner
  1227. self.api_version = api_version
  1228. self.default_storage_class = str(default_storage_class).lower()
  1229. self.kubeconfig = kubeconfig
  1230. self.mount_options = mount_options
  1231. self.reclaim_policy = reclaim_policy
  1232. self.data = {}
  1233. self.create_dict()
  1234. def create_dict(self):
  1235. ''' instantiates a storageclass dict '''
  1236. self.data['apiVersion'] = self.api_version
  1237. self.data['kind'] = 'StorageClass'
  1238. self.data['metadata'] = {}
  1239. self.data['metadata']['name'] = self.name
  1240. self.data['metadata']['annotations'] = {}
  1241. if self.annotations is not None:
  1242. self.data['metadata']['annotations'] = self.annotations
  1243. self.data['metadata']['annotations']['storageclass.beta.kubernetes.io/is-default-class'] = \
  1244. self.default_storage_class
  1245. self.data['provisioner'] = self.provisioner
  1246. self.data['parameters'] = {}
  1247. if self.parameters is not None:
  1248. self.data['parameters'].update(self.parameters)
  1249. # default to aws if no params were passed
  1250. else:
  1251. self.data['parameters']['type'] = 'gp2'
  1252. self.data['mountOptions'] = self.mount_options or []
  1253. if self.reclaim_policy is not None:
  1254. self.data['reclaimPolicy'] = self.reclaim_policy
  1255. # pylint: disable=too-many-instance-attributes,too-many-public-methods
  1256. class StorageClass(Yedit):
  1257. ''' Class to model the oc storageclass object '''
  1258. annotations_path = "metadata.annotations"
  1259. provisioner_path = "provisioner"
  1260. parameters_path = "parameters"
  1261. mount_options_path = "mountOptions"
  1262. reclaim_policy_path = "reclaimPolicy"
  1263. kind = 'StorageClass'
  1264. def __init__(self, content):
  1265. '''StorageClass constructor'''
  1266. super(StorageClass, self).__init__(content=content)
  1267. def get_annotations(self):
  1268. ''' get a list of ports '''
  1269. return self.get(StorageClass.annotations_path) or {}
  1270. def get_parameters(self):
  1271. ''' get the service selector'''
  1272. return self.get(StorageClass.parameters_path) or {}
  1273. def get_mount_options(self):
  1274. ''' get mount options'''
  1275. return self.get(StorageClass.mount_options_path) or []
  1276. def get_reclaim_policy(self):
  1277. ''' get reclaim policy'''
  1278. return self.get(StorageClass.reclaim_policy_path)
  1279. # -*- -*- -*- End included fragment: lib/storageclass.py -*- -*- -*-
  1280. # -*- -*- -*- Begin included fragment: class/oc_storageclass.py -*- -*- -*-
  1281. # pylint: disable=too-many-instance-attributes
  1282. class OCStorageClass(OpenShiftCLI):
  1283. ''' Class to wrap the oc command line tools '''
  1284. kind = 'storageclass'
  1285. # pylint allows 5
  1286. # pylint: disable=too-many-arguments
  1287. def __init__(self,
  1288. config,
  1289. verbose=False):
  1290. ''' Constructor for OCStorageClass '''
  1291. super(OCStorageClass, self).__init__(None, kubeconfig=config.kubeconfig, verbose=verbose)
  1292. self.config = config
  1293. self.storage_class = None
  1294. def exists(self):
  1295. ''' return whether a storageclass exists'''
  1296. if self.storage_class:
  1297. return True
  1298. return False
  1299. def get(self):
  1300. '''return storageclass '''
  1301. result = self._get(self.kind, self.config.name)
  1302. if result['returncode'] == 0:
  1303. self.storage_class = StorageClass(content=result['results'][0])
  1304. elif '\"%s\" not found' % self.config.name in result['stderr']:
  1305. result['returncode'] = 0
  1306. result['results'] = [{}]
  1307. return result
  1308. def delete(self):
  1309. '''delete the object'''
  1310. return self._delete(self.kind, self.config.name)
  1311. def create(self):
  1312. '''create the object'''
  1313. return self._create_from_content(self.config.name, self.config.data)
  1314. def update(self):
  1315. '''update the object'''
  1316. # parameters are currently unable to be updated. need to delete and recreate
  1317. self.delete()
  1318. # pause here and attempt to wait for delete.
  1319. # Better option would be to poll
  1320. time.sleep(5)
  1321. return self.create()
  1322. def needs_update(self):
  1323. ''' verify an update is needed '''
  1324. # check if params have updated
  1325. if self.storage_class.get_parameters() != self.config.parameters:
  1326. return True
  1327. for anno_key, anno_value in self.storage_class.get_annotations().items():
  1328. if 'is-default-class' in anno_key and anno_value != self.config.default_storage_class:
  1329. return True
  1330. # check if mount options have updated
  1331. if set(self.storage_class.get_mount_options()) != set(self.config.mount_options):
  1332. return True
  1333. # check if reclaim policy has been updated
  1334. if self.storage_class.get_reclaim_policy() != self.config.reclaim_policy:
  1335. return True
  1336. return False
  1337. @staticmethod
  1338. def provisioner_name_qualified(provisioner_name):
  1339. pattern = re.compile(r'^[a-z0-9A-Z-_.]+\/[a-z0-9A-Z-_.]+$')
  1340. return pattern.match(provisioner_name)
  1341. @staticmethod
  1342. # pylint: disable=too-many-return-statements,too-many-branches
  1343. # TODO: This function should be refactored into its individual parts.
  1344. def run_ansible(params, check_mode):
  1345. '''run the oc_storageclass module'''
  1346. # Make sure that the provisioner is fully qualified before using it
  1347. # E.g. if 'aws-efs' is provided as a provisioner, convert it to 'kubernetes.io/aws-efs'
  1348. # but if the name is already qualified (e.g. 'openshift.org/aws-efs') then leave it be.
  1349. raw_provisioner_name = params['provisioner']
  1350. if OCStorageClass.provisioner_name_qualified(raw_provisioner_name):
  1351. qualified_provisioner_name = raw_provisioner_name
  1352. else:
  1353. qualified_provisioner_name = "kubernetes.io/{}".format(params['provisioner'])
  1354. rconfig = StorageClassConfig(params['name'],
  1355. provisioner=qualified_provisioner_name,
  1356. parameters=params['parameters'],
  1357. annotations=params['annotations'],
  1358. api_version="storage.k8s.io/{}".format(params['api_version']),
  1359. default_storage_class=params.get('default_storage_class', 'false'),
  1360. kubeconfig=params['kubeconfig'],
  1361. mount_options=params['mount_options'],
  1362. reclaim_policy=params['reclaim_policy']
  1363. )
  1364. oc_sc = OCStorageClass(rconfig, verbose=params['debug'])
  1365. state = params['state']
  1366. api_rval = oc_sc.get()
  1367. #####
  1368. # Get
  1369. #####
  1370. if state == 'list':
  1371. return {'changed': False, 'results': api_rval['results'], 'state': 'list'}
  1372. ########
  1373. # Delete
  1374. ########
  1375. if state == 'absent':
  1376. if oc_sc.exists():
  1377. if check_mode:
  1378. return {'changed': True, 'msg': 'Would have performed a delete.'}
  1379. api_rval = oc_sc.delete()
  1380. return {'changed': True, 'results': api_rval, 'state': 'absent'}
  1381. return {'changed': False, 'state': 'absent'}
  1382. if state == 'present':
  1383. ########
  1384. # Create
  1385. ########
  1386. if not oc_sc.exists():
  1387. if check_mode:
  1388. return {'changed': True, 'msg': 'Would have performed a create.'}
  1389. # Create it here
  1390. api_rval = oc_sc.create()
  1391. if api_rval['returncode'] != 0:
  1392. return {'failed': True, 'msg': api_rval}
  1393. # return the created object
  1394. api_rval = oc_sc.get()
  1395. if api_rval['returncode'] != 0:
  1396. return {'failed': True, 'msg': api_rval}
  1397. return {'changed': True, 'results': api_rval, 'state': 'present'}
  1398. ########
  1399. # Update
  1400. ########
  1401. if oc_sc.needs_update():
  1402. api_rval = oc_sc.update()
  1403. if api_rval['returncode'] != 0:
  1404. return {'failed': True, 'msg': api_rval}
  1405. # return the created object
  1406. api_rval = oc_sc.get()
  1407. if api_rval['returncode'] != 0:
  1408. return {'failed': True, 'msg': api_rval}
  1409. return {'changed': True, 'results': api_rval, 'state': 'present'}
  1410. return {'changed': False, 'results': api_rval, 'state': 'present'}
  1411. return {'failed': True,
  1412. 'changed': False,
  1413. 'msg': 'Unknown state passed. %s' % state,
  1414. 'state': 'unknown'}
  1415. # -*- -*- -*- End included fragment: class/oc_storageclass.py -*- -*- -*-
  1416. # -*- -*- -*- Begin included fragment: ansible/oc_storageclass.py -*- -*- -*-
  1417. def main():
  1418. '''
  1419. ansible oc module for storageclass
  1420. '''
  1421. module = AnsibleModule(
  1422. argument_spec=dict(
  1423. kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'),
  1424. state=dict(default='present', type='str', choices=['present', 'absent', 'list']),
  1425. debug=dict(default=False, type='bool'),
  1426. name=dict(default=None, type='str'),
  1427. annotations=dict(default=None, type='dict'),
  1428. parameters=dict(default=None, type='dict'),
  1429. provisioner=dict(required=True, type='str'),
  1430. api_version=dict(default='v1', type='str'),
  1431. default_storage_class=dict(default="false", type='str'),
  1432. mount_options=dict(default=None, type='list'),
  1433. reclaim_policy=dict(default=None, type='str'),
  1434. ),
  1435. supports_check_mode=True,
  1436. )
  1437. rval = OCStorageClass.run_ansible(module.params, module.check_mode)
  1438. if 'failed' in rval:
  1439. return module.fail_json(**rval)
  1440. return module.exit_json(**rval)
  1441. if __name__ == '__main__':
  1442. main()
  1443. # -*- -*- -*- End included fragment: ansible/oc_storageclass.py -*- -*- -*-