From d3cd981326c98984117ae6fd768d63c39257de69 Mon Sep 17 00:00:00 2001 From: Kenny Woodson Date: Mon, 3 Jul 2017 16:33:30 -0400 Subject: Adding storageclass support to lib_openshift. --- roles/lib_openshift/src/ansible/oc_storageclass.py | 32 +++++ roles/lib_openshift/src/class/oc_storageclass.py | 147 +++++++++++++++++++++ roles/lib_openshift/src/doc/storageclass | 86 ++++++++++++ roles/lib_openshift/src/lib/storageclass.py | 72 ++++++++++ roles/lib_openshift/src/sources.yml | 11 ++ .../src/test/integration/oc_storageclass.yml | 87 ++++++++++++ .../src/test/unit/test_oc_storageclass.py | 93 +++++++++++++ 7 files changed, 528 insertions(+) create mode 100644 roles/lib_openshift/src/ansible/oc_storageclass.py create mode 100644 roles/lib_openshift/src/class/oc_storageclass.py create mode 100644 roles/lib_openshift/src/doc/storageclass create mode 100644 roles/lib_openshift/src/lib/storageclass.py create mode 100755 roles/lib_openshift/src/test/integration/oc_storageclass.yml create mode 100755 roles/lib_openshift/src/test/unit/test_oc_storageclass.py (limited to 'roles/lib_openshift/src') diff --git a/roles/lib_openshift/src/ansible/oc_storageclass.py b/roles/lib_openshift/src/ansible/oc_storageclass.py new file mode 100644 index 000000000..2bd8f18d5 --- /dev/null +++ b/roles/lib_openshift/src/ansible/oc_storageclass.py @@ -0,0 +1,32 @@ +# pylint: skip-file +# flake8: noqa + +def main(): + ''' + ansible oc module for storageclass + ''' + + module = AnsibleModule( + argument_spec=dict( + kubeconfig=dict(default='/etc/origin/master/admin.kubeconfig', type='str'), + state=dict(default='present', type='str', choices=['present', 'absent', 'list']), + debug=dict(default=False, type='bool'), + name=dict(default=None, type='str'), + annotations=dict(default=None, type='dict'), + parameters=dict(default=None, type='dict'), + provisioner=dict(default='aws-ebs', type='str', choices=['aws-ebs', 'gce-pd', 'glusterfs', 'cinder']), + api_version=dict(default='v1', type='str'), + default_storage_class=dict(default="false", type='str'), + ), + supports_check_mode=True, + ) + + rval = OCStorageClass.run_ansible(module.params, module.check_mode) + if 'failed' in rval: + return module.fail_json(**rval) + + return module.exit_json(**rval) + + +if __name__ == '__main__': + main() diff --git a/roles/lib_openshift/src/class/oc_storageclass.py b/roles/lib_openshift/src/class/oc_storageclass.py new file mode 100644 index 000000000..6c62c7bd8 --- /dev/null +++ b/roles/lib_openshift/src/class/oc_storageclass.py @@ -0,0 +1,147 @@ +# pylint: skip-file +# flake8: noqa + +# pylint: disable=too-many-instance-attributes +class OCStorageClass(OpenShiftCLI): + ''' Class to wrap the oc command line tools ''' + kind = 'sc' + + # pylint allows 5 + # pylint: disable=too-many-arguments + def __init__(self, + config, + verbose=False): + ''' Constructor for OCStorageClass ''' + super(OCStorageClass, self).__init__(None, kubeconfig=config.kubeconfig, verbose=verbose) + self.config = config + self.storage_class = None + + def exists(self): + ''' return whether a storageclass exists''' + if self.storage_class: + return True + + return False + + def get(self): + '''return storageclass ''' + result = self._get(self.kind, self.config.name) + if result['returncode'] == 0: + self.storage_class = StorageClass(content=result['results'][0]) + elif '\"%s\" not found' % self.config.name in result['stderr']: + result['returncode'] = 0 + result['results'] = [{}] + + return result + + def delete(self): + '''delete the object''' + return self._delete(self.kind, self.config.name) + + def create(self): + '''create the object''' + return self._create_from_content(self.config.name, self.config.data) + + def update(self): + '''update the object''' + # parameters are currently unable to be updated. need to delete and recreate + self.delete() + return self.create() + + def needs_update(self): + ''' verify an update is needed ''' + # check if params have updated + if self.storage_class.get_parameters() == self.config.parameters: + return False + + return True + + @staticmethod + # pylint: disable=too-many-return-statements,too-many-branches + # TODO: This function should be refactored into its individual parts. + def run_ansible(params, check_mode): + '''run the ansible idempotent code''' + + rconfig = StorageClassConfig(params['name'], + provisioner="kubernetes.io/{}".format(params['provisioner']), + parameters=params['parameters'], + annotations=params['annotations'], + api_version="storage.k8s.io/{}".format(params['api_version']), + default_sc=params['default_storage_class'], + kubeconfig=params['kubeconfig'], + ) + + oc_sc = OCStorageClass(rconfig, verbose=params['debug']) + + state = params['state'] + + api_rval = oc_sc.get() + + ##### + # Get + ##### + if state == 'list': + return {'changed': False, 'results': api_rval['results'], 'state': 'list'} + + ######## + # Delete + ######## + if state == 'absent': + if oc_sc.exists(): + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a delete.'} + + api_rval = oc_sc.delete() + + return {'changed': True, 'results': api_rval, 'state': 'absent'} + + return {'changed': False, 'state': 'absent'} + + if state == 'present': + ######## + # Create + ######## + if not oc_sc.exists(): + + if check_mode: + return {'changed': True, 'msg': 'Would have performed a create.'} + + # Create it here + api_rval = oc_sc.create() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + # return the created object + api_rval = oc_sc.get() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': 'present'} + + ######## + # Update + ######## + if oc_sc.needs_update(): + api_rval = oc_sc.update() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + # return the created object + api_rval = oc_sc.get() + + if api_rval['returncode'] != 0: + return {'failed': True, 'msg': api_rval} + + return {'changed': True, 'results': api_rval, 'state': 'present'} + + return {'changed': False, 'results': api_rval, 'state': 'present'} + + + return {'failed': True, + 'changed': False, + 'msg': 'Unknown state passed. %s' % state, + 'state': 'unknown'} diff --git a/roles/lib_openshift/src/doc/storageclass b/roles/lib_openshift/src/doc/storageclass new file mode 100644 index 000000000..278c0b5b3 --- /dev/null +++ b/roles/lib_openshift/src/doc/storageclass @@ -0,0 +1,86 @@ +# flake8: noqa +# pylint: skip-file + +DOCUMENTATION = ''' +--- +module: oc_storageclass +short_description: Create, modify, and idempotently manage openshift storageclasses. +description: + - Manage openshift storageclass objects programmatically. +options: + state: + description: + - State represents whether to create, modify, delete, or list + required: False + default: present + choices: ["present", "absent", "list"] + aliases: [] + kubeconfig: + description: + - The path for the kubeconfig file to use for authentication + required: false + default: /etc/origin/master/admin.kubeconfig + aliases: [] + debug: + description: + - Turn on debug output. + required: false + default: False + aliases: [] + name: + description: + - Name of the object that is being queried. + required: false + default: None + aliases: [] + provisioner: + description: + - Any annotations to add to the storageclass + required: false + default: 'aws-ebs' + aliases: [] + default_storage_class: + description: + - Whether or not this is the default storage class + required: false + default: False + aliases: [] + parameters: + description: + - A dictionary with the parameters to configure the storageclass. This will be based on provisioner + required: false + default: None + aliases: [] + api_version: + description: + - The api version. + required: false + default: v1 + aliases: [] +author: +- "Kenny Woodson " +extends_documentation_fragment: [] +''' + +EXAMPLES = ''' +- name: get storageclass + run_once: true + oc_storageclass: + name: gp2 + state: list + register: registry_sc_out + +- name: create the storageclass + oc_storageclass: + run_once: true + name: gp2 + parameters: + type: gp2 + encrypted: 'true' + kmsKeyId: '' + provisioner: aws-ebs + default_sc: False + register: sc_out + notify: + - restart openshift master services +''' diff --git a/roles/lib_openshift/src/lib/storageclass.py b/roles/lib_openshift/src/lib/storageclass.py new file mode 100644 index 000000000..958bb2876 --- /dev/null +++ b/roles/lib_openshift/src/lib/storageclass.py @@ -0,0 +1,72 @@ +# pylint: skip-file +# flake8: noqa + + +# pylint: disable=too-many-instance-attributes +class StorageClassConfig(object): + ''' Handle service options ''' + # pylint: disable=too-many-arguments + def __init__(self, + name, + provisioner=None, + parameters=None, + annotations=None, + default_sc="false", + api_version='v1', + kubeconfig='/etc/origin/master/admin.kubeconfig'): + ''' constructor for handling storageclass options ''' + self.name = name + self.parameters = parameters + self.annotations = annotations + self.provisioner = provisioner + self.api_version = api_version + self.default_sc = default_sc + self.kubeconfig = kubeconfig + self.data = {} + + self.create_dict() + + def create_dict(self): + ''' instantiates a storageclass dict ''' + self.data['apiVersion'] = self.api_version + self.data['kind'] = 'StorageClass' + self.data['metadata'] = {} + self.data['metadata']['name'] = self.name + + self.data['metadata']['annotations'] = {} + self.data['metadata']['annotations']['storageclass.beta.kubernetes.io/is-default-class'] = self.default_sc + + if self.provisioner is None: + self.data['provisioner'] = 'kubernetes.io/aws-ebs' + else: + self.data['provisioner'] = self.provisioner + + self.data['parameters'] = {} + if self.parameters is not None: + self.data['parameters'].update(self.parameters) + + # default to aws if no params were passed + else: + self.data['parameters']['type'] = 'gp2' + + + +# pylint: disable=too-many-instance-attributes,too-many-public-methods +class StorageClass(Yedit): + ''' Class to model the oc storageclass object ''' + annotations_path = "metadata.annotations" + provisioner_path = "provisioner" + parameters_path = "parameters" + kind = 'StorageClass' + + def __init__(self, content): + '''StorageClass constructor''' + super(StorageClass, self).__init__(content=content) + + def get_annotations(self): + ''' get a list of ports ''' + return self.get(StorageClass.annotations_path) or {} + + def get_parameters(self): + ''' get the service selector''' + return self.get(StorageClass.parameters_path) or {} diff --git a/roles/lib_openshift/src/sources.yml b/roles/lib_openshift/src/sources.yml index 9fa2a6c0e..e9b6bf261 100644 --- a/roles/lib_openshift/src/sources.yml +++ b/roles/lib_openshift/src/sources.yml @@ -263,6 +263,17 @@ oc_service.py: - class/oc_service.py - ansible/oc_service.py +oc_storageclass.py: +- doc/generated +- doc/license +- lib/import.py +- doc/storageclass +- ../../lib_utils/src/class/yedit.py +- lib/base.py +- lib/storageclass.py +- class/oc_storageclass.py +- ansible/oc_storageclass.py + oc_user.py: - doc/generated - doc/license diff --git a/roles/lib_openshift/src/test/integration/oc_storageclass.yml b/roles/lib_openshift/src/test/integration/oc_storageclass.yml new file mode 100755 index 000000000..c82f9dedb --- /dev/null +++ b/roles/lib_openshift/src/test/integration/oc_storageclass.yml @@ -0,0 +1,87 @@ +#!/usr/bin/ansible-playbook --module-path=../../../library/ +# ./oc_storageclass.yml -M ../../../library -e "cli_master_test=$OPENSHIFT_MASTER +--- +- hosts: "{{ cli_master_test }}" + gather_facts: no + user: root + tasks: + - name: create a storageclass + oc_storageclass: + name: testsc + parameters: + type: gp2 + default_storage_class: "true" + register: sc_out + - debug: var=sc_out + + - assert: + that: + - "sc_out.results.results[0]['metadata']['name'] == 'testsc'" + - sc_out.changed + - "sc_out.results.results[0]['parameters']['type'] == 'gp2'" + msg: storageclass create failed. + + # Test idempotent create + - name: NOOP create the storageclass + oc_storageclass: + name: testsc + parameters: + type: gp2 + default_storage_class: "true" + register: sc_out + + - assert: + that: + - "sc_out.results.results[0]['metadata']['name'] == 'testsc'" + - sc_out.changed == False + msg: storageclass create failed. No changes expected + + - name: test list storageclass + oc_storageclass: + name: testsc + state: list + register: sc_out + - debug: var=sc_out + + - assert: + that: "sc_out.results[0]['metadata']['name'] == 'testsc'" + msg: storageclass list failed + + - name: update the storageclass + oc_storageclass: + name: testsc + parameters: + type: gp2 + encrypted: "true" + default_storage_class: "true" + register: sc_out + + - assert: + that: "sc_out.results.results[0]['parameters']['encrypted'] == 'true'" + msg: storageclass update failed + + - name: oc delete storageclass + oc_storageclass: + name: testsc + state: absent + register: sc_out + - debug: var=sc_out + + - assert: + that: + - "sc_out.results['returncode'] == 0" + - "sc_out.results.results == {}" + msg: storageclass delete failed + + - name: oc get storageclass + oc_storageclass: + name: testsc + state: list + register: sc_out + - debug: var=sc_out + + - assert: + that: + - sc_out.changed == False + - "sc_out.results == [{}]" + msg: storageclass get failed diff --git a/roles/lib_openshift/src/test/unit/test_oc_storageclass.py b/roles/lib_openshift/src/test/unit/test_oc_storageclass.py new file mode 100755 index 000000000..f3cc4a76f --- /dev/null +++ b/roles/lib_openshift/src/test/unit/test_oc_storageclass.py @@ -0,0 +1,93 @@ +''' + Unit tests for oc serviceaccount +''' + +import os +import sys +import unittest +import mock + +# Removing invalid variable names for tests so that I can +# keep them brief +# pylint: disable=invalid-name,no-name-in-module +# Disable import-error b/c our libraries aren't loaded in jenkins +# pylint: disable=import-error +# place class in our python path +module_path = os.path.join('/'.join(os.path.realpath(__file__).split('/')[:-4]), 'library') # noqa: E501 +sys.path.insert(0, module_path) +from oc_storageclass import OCStorageClass # noqa: E402 + + +class OCStorageClassTest(unittest.TestCase): + ''' + Test class for OCStorageClass + ''' + params = {'kubeconfig': '/etc/origin/master/admin.kubeconfig', + 'state': 'present', + 'debug': False, + 'name': 'testsc', + 'provisioner': 'kubernetes.io/aws-ebs', + 'annotations': {'storageclass.beta.kubernetes.io/is-default-class': "true"}, + 'parameters': {'type': 'gp2'}, + 'api_version': 'v1', + 'default_storage_class': 'true'} + + @mock.patch('oc_storageclass.locate_oc_binary') + @mock.patch('oc_storageclass.Utils.create_tmpfile_copy') + @mock.patch('oc_storageclass.OCStorageClass._run') + def test_adding_a_storageclass(self, mock_cmd, mock_tmpfile_copy, mock_oc_binary): + ''' Testing adding a storageclass ''' + + # Arrange + + # run_ansible input parameters + + valid_result_json = '''{ + "kind": "StorageClass", + "apiVersion": "v1", + "metadata": { + "name": "testsc", + "selfLink": "/apis/storage.k8s.io/v1/storageclasses/gp2", + "uid": "4d8320c9-e66f-11e6-8edc-0eece8f2ce22", + "resourceVersion": "2828", + "creationTimestamp": "2017-01-29T22:07:19Z", + "annotations": {"storageclass.beta.kubernetes.io/is-default-class": "true"} + }, + "provisioner": "kubernetes.io/aws-ebs", + "parameters": {"type": "gp2"} + }''' + + # Return values of our mocked function call. These get returned once per call. + mock_cmd.side_effect = [ + # First call to mock + (1, '', 'Error from server: storageclass "testsc" not found'), + + # Second call to mock + (0, 'storageclass "testsc" created', ''), + + # Third call to mock + (0, valid_result_json, ''), + ] + + mock_oc_binary.side_effect = [ + 'oc' + ] + + mock_tmpfile_copy.side_effect = [ + '/tmp/mocked_kubeconfig', + ] + + # Act + results = OCStorageClass.run_ansible(OCStorageClassTest.params, False) + + # Assert + self.assertTrue(results['changed']) + self.assertEqual(results['results']['returncode'], 0) + self.assertEqual(results['state'], 'present') + + # Making sure our mock was called as we expected + mock_cmd.assert_has_calls([ + mock.call(['oc', 'get', 'sc', 'testsc', '-o', 'json'], None), + mock.call(['oc', 'create', '-f', mock.ANY], None), + mock.call(['oc', 'get', 'sc', 'testsc', '-o', 'json'], None), + ]) -- cgit v1.2.1