diff --git a/lib/ansible/modules/cloud/hpe/hpe_icsp_os_deployment.py b/lib/ansible/modules/cloud/hpe/hpe_icsp_os_deployment.py new file mode 100755 index 00000000000000..4c43e79f93f352 --- /dev/null +++ b/lib/ansible/modules/cloud/hpe/hpe_icsp_os_deployment.py @@ -0,0 +1,217 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'curated', + 'metadata_version': '1.0'} + +DOCUMENTATION = ''' +--- +module: hpe_icsp_os_deployment +short_description: Deploy the operating system on a server using HPE ICsp. +description: + - Deploy the operating system on a server based on the available ICsp OS build plan. +requirements: + - "python >= 2.7.9" + - "hpICsp >= 1.0.2" +version_added: "2.3" +author: + - "Tiago Totti (@tiagomtotti)" + - "Chakravarthy Racharla (@ChakruHP)" +options: + api_version: + description: + - ICsp API version. + required: false + default: 300 + icsp_host: + description: + - ICsp hostname. + required: true + username: + description: + - ICsp username. + required: true + password: + description: + - ICsp password. + required: true + server_id: + description: + - Server ID. + required: true + os_build_plan: + description: + - OS Build plan. + required: true + custom_attributes: + description: + - Custom Attributes. + required: false + default: null + personality_data: + description: + - Personality Data. + required: false + default: null +''' + +EXAMPLES = ''' +- name: Deploy OS + hpe_icsp_os_deployment: + icsp_host: "{{ icsp }}" + username: "{{ icsp_username }}" + password: "{{ icsp_password }}" + server_id: "{{ server_profile.serialNumber }}" + os_build_plan: "{{ os_build_plan }}" + custom_attributes: "{{ osbp_custom_attributes }}" + personality_data: "{{ network_config }}" + delegate_to: localhost +''' + +RETURN = ''' +icsp_server: + description: Has the facts about the server that was provisioned with ICsp. + returned: When the module runs successfully, but can be null. + type: complex +''' + +from future import standard_library + +standard_library.install_aliases() + +import time +import hpICsp +from urllib.parse import quote +from ansible.module_utils.basic import AnsibleModule + + +def get_build_plan(con, bp_name): + search_uri = '/rest/index/resources?filter="name=\'' + quote(bp_name) + '\'"&category=osdbuildplan' + search_result = con.get(search_uri) + + if search_result['count'] > 0 and search_result['members'][0]['name'] == bp_name: + return search_result['members'][0] + return None + + +def get_server_by_serial(con, serial_number): + search_uri = '/rest/index/resources?category=osdserver&query=\'osdServerSerialNumber:\"' + serial_number + '\"\'' + search_result = con.get(search_uri) + if search_result['count'] > 0: + same_serial_number = search_result['members'][0]['attributes']['osdServerSerialNumber'] == serial_number + + if same_serial_number: + server_id = search_result['members'][0]['attributes']['osdServerId'] + server = {'uri': '/rest/os-deployment-servers/' + server_id} + return server + + return None + + +def deploy_server(module): + # Credentials + icsp_host = module.params['icsp_host'] + icsp_api_version = module.params['api_version'] + username = module.params['username'] + password = module.params['password'] + + # Build Plan Options + server_id = module.params['server_id'] + os_build_plan = module.params['os_build_plan'] + custom_attributes = module.params['custom_attributes'] + personality_data = module.params['personality_data'] + con = hpICsp.connection(icsp_host, icsp_api_version) + + # Create objects for all necessary resources. + credential = {'userName': username, 'password': password} + con.login(credential) + + bp = hpICsp.buildPlans(con) + jb = hpICsp.jobs(con) + sv = hpICsp.servers(con) + + bp = get_build_plan(con, os_build_plan) + + if bp is None: + return module.fail_json(msg='Cannot find OS Build plan: ' + os_build_plan) + + timeout = 600 + while True: + server = get_server_by_serial(con, server_id) + if server: + break + if timeout < 0: + module.fail_json(msg='Cannot find server in ICSP.') + return + timeout -= 30 + time.sleep(30) + + server = sv.get_server(server['uri']) + if server['state'] == 'OK': + return module.exit_json(changed=False, msg="Server already deployed.", ansible_facts={'icsp_server': server}) + + if custom_attributes: + ca_list = [] + + for ca in custom_attributes: + ca_list.append({ + 'key': list(ca.keys())[0], + 'values': [{'scope': 'server', 'value': str(list(ca.values())[0])}]}) + + ca_list.extend(server['customAttributes']) + server['customAttributes'] = ca_list + sv.update_server(server) + + server_data = {"serverUri": server['uri'], "personalityData": None} + + build_plan_body = {"osbpUris": [bp['uri']], "serverData": [server_data], "stepNo": 1} + + hpICsp.common.monitor_execution(jb.add_job(build_plan_body), jb) + + # If the playbook included network personalization, update the server to include it + if personality_data: + server_data['personalityData'] = personality_data + network_config = {"serverData": [server_data]} + # Monitor the execution of a nework personalization job. + hpICsp.common.monitor_execution(jb.add_job(network_config), jb) + + server = sv.get_server(server['uri']) + return module.exit_json(changed=True, msg='OS Deployed Successfully.', ansible_facts={'icsp_server': server}) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + api_version=dict(type='int', default=300), + icsp_host=dict(required=True, type='str'), + username=dict(required=True, type='str'), + password=dict(required=True, type='str', no_log=True), + server_id=dict(required=True, type='str'), + os_build_plan=dict(required=True, type='str'), + custom_attributes=dict(required=False, type='list', default=None), + personality_data=dict(required=False, type='dict', default=None) + )) + + deploy_server(module) + + +if __name__ == '__main__': + main() diff --git a/lib/ansible/modules/cloud/hpe/hpe_icsp_server.py b/lib/ansible/modules/cloud/hpe/hpe_icsp_server.py new file mode 100755 index 00000000000000..0555647c3afa95 --- /dev/null +++ b/lib/ansible/modules/cloud/hpe/hpe_icsp_server.py @@ -0,0 +1,293 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'curated', + 'metadata_version': '1.0'} + +DOCUMENTATION = ''' +--- +module: hpe_icsp_server +short_description: Adds, removes and configures servers in ICsp. +description: + - This module allows you to add, remove and configure servers in the Insight Control Server Provisioning (ICsp). + In ICsp, a server, often referred to as a Target Server, is a physical ProLiant server or a virtual machine that + can have actions taken upon it. +requirements: + - "python >= 2.7.9" + - "hpICsp >= 1.0.2" +version_added: "2.3" +author: + - "Tiago Totti (@tiagomtotti)" +options: + state: + description: + - Indicates the desired state for the ICsp server. + 'present' will register the resource on ICsp. + 'absent' will remove the resource from ICsp, if it exists. + 'network_configured' will set the network configuration. + choices: ['present', 'absent', 'network_configured'] + api_version: + description: + - ICsp API version. + required: false + default: 300 + icsp_host: + description: + - ICsp hostname. + required: true + username: + description: + - ICsp username. + required: true + password: + description: + - ICsp password. + required: true + server_ipAddress: + description: + - The IP address of the iLO of the server. + required: true + server_username: + description: + - The username required to log into the server's iLO. + required: true + server_password: + description: + - The password required to log into the server's iLO + required: true + server_port: + description: + - The iLO port to use when logging in. + default: + - 443 + required: false + server_personality_data: + description: + - Additional data to send to ICsp. + required: false +''' + +EXAMPLES = ''' + - name: Ensure the server is registered in ICsp + hpe_icsp_server: + icsp_host: "{{icsp_host}}" + username: "{{icsp_username}}" + password: "{{icsp_password}}" + server_ipAddress: "{{server_iLO_ip}}" + server_username: "Admin" + server_password: "admin" + state: present + delegate_to: localhost + + - name: Set the network configuration + hpe_icsp_server: + icsp_host: "{{ icsp }}" + username: "{{ icsp_username }}" + password: "{{ icsp_password }}" + server_ipAddress: "{{ server_ipAddress }}" + server_username: "{{ server_username }}" + server_password: "{{ server_password }}" + server_personality_data: "{{ network_config }}" + state: network_configured + delegate_to: localhost + + - name: Ensure the server is removed from ICsp + hpe_icsp_server: + icsp_host: "{{icsp_host}}" + username: "{{icsp_username}}" + password: "{{icsp_password}}" + server_ipAddress: "{{server_iLO_ip}}" + server_username: "Admin" + server_password: "admin" + state: absent + delegate_to: localhost +''' + +RETURN = ''' +target_server: + description: Has the facts about the server that was added to ICsp. + returned: On states 'present' and 'network_configured' . Can be null. + type: complex +''' + +import json +import hpICsp +from hpICsp.exceptions import HPICspException +from ansible.module_utils.basic import AnsibleModule + + +class ICspServerModule(object): + SERVER_CREATED = "Server created: '{}'" + SERVER_ALREADY_PRESENT = "Server is already present." + SERVER_ALREADY_ABSENT = "Target server is already absent in ICsp." + SERVER_REMOVED = "Server '{}' removed successfully from ICsp." + CUSTOM_ATTR_NETWORK_UPDATED = 'Network Custom Attribute Updated.' + SERVER_NOT_FOUND = "Target server is not present in ICsp." + SERVER_PERSONALITY_DATA_REQUIRED = 'server_personality_data must be informed.' + + argument_spec = dict( + # Connection + api_version=dict(type='int', default=300), + icsp_host=dict(required=True, type='str'), + username=dict(required=True, type='str'), + password=dict(required=True, type='str', no_log=True), + # options + state=dict( + required=True, + choices=['present', 'absent', 'network_configured'] + ), + # server data + server_ipAddress=dict(required=True, type='str'), + server_username=dict(required=True, type='str'), + server_password=dict(required=True, type='str', no_log=True), + server_port=dict(type='int', default=443), + server_personality_data=dict(required=False, type='dict') + ) + + def __init__(self): + self.module = AnsibleModule(argument_spec=self.argument_spec, supports_check_mode=False) + self.connection = self.__authenticate() + + def run(self): + + state = self.module.params['state'] + ilo_address = self.module.params['server_ipAddress'] + target_server = self.__get_server_by_ilo_address(ilo_address) + + if state == 'present': + self.__present(target_server) + + elif state == 'absent': + self.__absent(target_server) + + elif state == 'network_configured': + self.__configure_network(target_server) + + def __authenticate(self): + # Credentials + icsp_host = self.module.params['icsp_host'] + icsp_api_version = self.module.params['api_version'] + username = self.module.params['username'] + password = self.module.params['password'] + + con = hpICsp.connection(icsp_host, icsp_api_version) + + credential = {'userName': username, 'password': password} + con.login(credential) + return con + + def __present(self, target_server): + # check if server exists + if target_server: + return self.module.exit_json(changed=False, + msg=self.SERVER_ALREADY_PRESENT, + ansible_facts=dict(target_server=target_server)) + + return self._add_server() + + def __absent(self, target_server): + # check if server exists + if not target_server: + return self.module.exit_json(changed=False, msg=self.SERVER_ALREADY_ABSENT) + + server_uri = target_server['uri'] + servers_service = hpICsp.servers(self.connection) + + try: + servers_service.delete_server(server_uri) + return self.module.exit_json(changed=True, + msg=self.SERVER_REMOVED.format(server_uri)) + + except HPICspException as icsp_exe: + self.module.fail_json(msg=json.dumps(icsp_exe.__dict__)) + + except Exception as exception: + self.module.fail_json(msg='; '.join(str(e) for e in exception.args)) + + def __configure_network(self, target_server): + personality_data = self.module.params.get('server_personality_data') + + if not personality_data: + return self.module.fail_json(msg=self.SERVER_PERSONALITY_DATA_REQUIRED) + + # check if server exists + if not target_server: + return self.module.exit_json(changed=False, msg=self.SERVER_NOT_FOUND) + + server_data = {"serverUri": target_server['uri'], "personalityData": personality_data, "skipReboot": True} + network_config = {"serverData": [server_data], "failMode": None, "osbpUris": []} + + # Save nework personalization attribute, without running the job + self.__add_write_only_job(network_config) + + servers_service = hpICsp.servers(self.connection) + server = servers_service.get_server(target_server['uri']) + return self.module.exit_json(changed=True, + msg=self.CUSTOM_ATTR_NETWORK_UPDATED, + ansible_facts={'target_server': server}) + + def __add_write_only_job(self, body): + body = self.connection.post("/rest/os-deployment-jobs/?writeOnly=true", body) + return body + + def __get_server_by_ilo_address(self, ilo): + servers = self.connection.get("/rest/os-deployment-servers/?count=-1") + srv = self.__filter_by_ilo(servers['members'], ilo) + return srv + + def __filter_by_ilo(self, seq, value): + for srv in seq: + if srv['ilo']['ipAddress'] == value: + return srv + return None + + def _add_server(self): + ilo_address = self.module.params['server_ipAddress'] + + # Creates a JSON body for adding an iLo. + ilo_body = {'ipAddress': ilo_address, + 'username': self.module.params['server_username'], + 'password': self.module.params['server_password'], + 'port': self.module.params['server_port']} + + job_monitor = hpICsp.jobs(self.connection) + servers_service = hpICsp.servers(self.connection) + + # Monitor_execution is a utility method to watch job progress on the command line. + add_server_job = servers_service.add_server(ilo_body) + hpICsp.common.monitor_execution(add_server_job, job_monitor) + + # Python bindings throw an Exception when the status != ok + # So if we got this far, the job execution finished as expected + + # gets the target server added to ICsp to return on ansible facts + target_server = self.__get_server_by_ilo_address(ilo_address) + return self.module.exit_json(changed=True, + msg=self.SERVER_CREATED.format(target_server['uri']), + ansible_facts=dict(target_server=target_server)) + + +def main(): + ICspServerModule().run() + + +if __name__ == '__main__': + main() diff --git a/test/units/modules/cloud/hpe/test_hpe_icsp_os_deployment.py b/test/units/modules/cloud/hpe/test_hpe_icsp_os_deployment.py new file mode 100755 index 00000000000000..16ee7228206fcb --- /dev/null +++ b/test/units/modules/cloud/hpe/test_hpe_icsp_os_deployment.py @@ -0,0 +1,273 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import unittest +import mock +import ansible.modules.cloud.hpe.hpe_icsp_os_deployment +from copy import deepcopy + +MODULE_NAME = 'ansible.modules.cloud.hpe.hpe_icsp_os_deployment' + +TASK_OS_DEPLOYMENT = { + "icsp_host": "16.124.133.251", + "api_version": 300, + "username": "Administrator", + "password": "admin", + "server_id": "VCGYZ33007", + "os_build_plan": "RHEL 7.2 x64", + "personality_data": None, + "custom_attributes": None +} + +DEFAULT_SERVER = {"name": "SP-01", + "uri": "/uri/239", + "ilo": {"ipAddress": "16.124.135.239"}, + "state": "", + 'attributes': {'osdServerId': '123456', + 'osdServerSerialNumber': 'VCGYZ33007'}, + "customAttributes": []} + +DEFAULT_SERVER_UPDATED = {"name": "SP-01", + "uri": "/uri/239", + "ilo": {"ipAddress": "16.124.135.239"}, + "state": "OK", + "customAttributes": + [{'values': [{'scope': 'server', 'value': "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"}], + 'key': 'SSH_CERT'}]} + +DEFAULT_BUILD_PLAN = {"name": "RHEL 7.2 x64", "uri": "/rest/os-deployment-build-plans/222"} + + +class IcspOsDeploymentSpec(unittest.TestCase): + def setUp(self): + self.patcher_ansible_module = mock.patch(MODULE_NAME + '.AnsibleModule') + self.mock_ansible_module = self.patcher_ansible_module.start() + + self.mock_ansible_instance = mock.Mock() + self.mock_ansible_module.return_value = self.mock_ansible_instance + + self.patcher_icsp_service = mock.patch(MODULE_NAME + '.hpICsp') + self.mock_icsp = self.patcher_icsp_service.start() + + self.patcher_time_sleep = mock.patch('time.sleep', return_value=None) + self.mock_time_sleep = self.patcher_time_sleep.start() + + self.mock_connection = mock.Mock() + self.mock_connection.login.return_value = {} + self.mock_icsp.connection.return_value = self.mock_connection + + self.mock_icsp_common = mock.Mock() + self.mock_icsp.common.return_value = self.mock_icsp_common + + self.mock_icsp_jobs = mock.Mock() + self.mock_icsp.jobs.return_value = self.mock_icsp_jobs + + self.mock_server_service = mock.Mock() + self.mock_icsp.servers.return_value = self.mock_server_service + + self.mock_build_plans_service = mock.Mock() + self.mock_icsp.buildPlans.return_value = self.mock_build_plans_service + + def tearDown(self): + self.patcher_ansible_module.stop() + self.patcher_icsp_service.stop() + self.patcher_time_sleep.stop() + + def get_as_rest_collection(self, server): + return { + 'members': server, + 'count': len(server) + } + + def test_should_not_add_server_when_already_present(self): + server_already_deployed = dict(DEFAULT_SERVER, state="OK") + + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_BUILD_PLAN]), + self.get_as_rest_collection([DEFAULT_SERVER])] + + self.mock_server_service.get_server.return_value = server_already_deployed + + self.mock_ansible_instance.params = TASK_OS_DEPLOYMENT + + hpe_icsp_os_deployment.main() + + self.mock_time_sleep.assert_not_called() + + self.mock_ansible_instance.exit_json.assert_called_once_with( + changed=False, msg="Server already deployed.", ansible_facts={'icsp_server': server_already_deployed} + ) + + def test_should_fail_after_try_get_server_by_serial_21_times(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_BUILD_PLAN])] + + self.mock_server_service.get_server.return_value = DEFAULT_SERVER + + self.mock_ansible_instance.params = TASK_OS_DEPLOYMENT + + with mock.patch(MODULE_NAME + '.get_server_by_serial') as mock_get_srv_ser: + mock_get_srv_ser.return_value = None + hpe_icsp_os_deployment.main() + + times_sleep_called = self.mock_time_sleep.call_count + self.assertEqual(21, times_sleep_called) + + self.mock_ansible_instance.fail_json.assert_called_once_with(msg='Cannot find server in ICSP.') + + def test_should_deploy_server(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_BUILD_PLAN]), + self.get_as_rest_collection([DEFAULT_SERVER])] + + self.mock_server_service.get_server.side_effect = [DEFAULT_SERVER, DEFAULT_SERVER_UPDATED] + + self.mock_ansible_instance.params = TASK_OS_DEPLOYMENT + + hpe_icsp_os_deployment.main() + + times_sleep_called = self.mock_time_sleep.call_count + self.assertEqual(0, times_sleep_called) + + self.mock_ansible_instance.exit_json.assert_called_once_with(changed=True, msg='OS Deployed Successfully.', + ansible_facts={ + 'icsp_server': DEFAULT_SERVER_UPDATED}) + + def test_should_try_deploy_server_3_times(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_BUILD_PLAN]), + self.get_as_rest_collection([]), + self.get_as_rest_collection([]), + self.get_as_rest_collection([DEFAULT_SERVER])] + + self.mock_server_service.get_server.side_effect = [DEFAULT_SERVER, DEFAULT_SERVER_UPDATED] + + self.mock_ansible_instance.params = TASK_OS_DEPLOYMENT + + hpe_icsp_os_deployment.main() + + times_sleep_called = self.mock_time_sleep.call_count + self.assertEqual(2, times_sleep_called) + + self.mock_ansible_instance.exit_json.assert_called_once_with(changed=True, msg='OS Deployed Successfully.', + ansible_facts={ + 'icsp_server': DEFAULT_SERVER_UPDATED}) + + def test_should_fail_when_os_build_plan_not_found(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([]), + self.get_as_rest_collection([]), + self.get_as_rest_collection([]), + self.get_as_rest_collection([DEFAULT_SERVER])] + + self.mock_server_service.get_server.return_value = DEFAULT_SERVER + + self.mock_ansible_instance.params = TASK_OS_DEPLOYMENT + + hpe_icsp_os_deployment.main() + + self.mock_ansible_instance.fail_json.assert_called_once_with(msg='Cannot find OS Build plan: RHEL 7.2 x64') + + def test_should_update_server_when_task_include_network_personalization(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_BUILD_PLAN]), + self.get_as_rest_collection([DEFAULT_SERVER])] + + self.mock_server_service.get_server.side_effect = [DEFAULT_SERVER, DEFAULT_SERVER_UPDATED] + self.mock_icsp.common.monitor_execution.return_value = {} + self.mock_icsp_jobs.add_job.return_value = {"job mock return"} + + task_with_network_personalization = deepcopy(TASK_OS_DEPLOYMENT) + network_config = {"network_config": {"hostname": "test-web.io.fc.hpe.com", "domain": "demo.com"}} + task_with_network_personalization['personality_data'] = network_config + self.mock_ansible_instance.params = task_with_network_personalization + + hpe_icsp_os_deployment.main() + + server_data = {"serverUri": DEFAULT_SERVER['uri'], "personalityData": None} + network_config = {"serverData": [server_data]} + build_plan_body = {"osbpUris": [DEFAULT_BUILD_PLAN['uri']], "serverData": [server_data], "stepNo": 1} + + monitor_build_plan = mock.call(self.mock_icsp_jobs.add_job(build_plan_body), self.mock_icsp_jobs) + monitor_update_server = mock.call(self.mock_icsp_jobs.add_job(network_config), self.mock_icsp_jobs) + calls = [monitor_build_plan, monitor_update_server] + + self.mock_icsp.common.monitor_execution.assert_has_calls(calls) + + def test_should_update_server_when_task_include_custom_attributes(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_BUILD_PLAN]), + self.get_as_rest_collection([DEFAULT_SERVER])] + + self.mock_server_service.get_server.side_effect = [DEFAULT_SERVER, DEFAULT_SERVER_UPDATED] + self.mock_server_service.update_server.return_value = DEFAULT_SERVER_UPDATED + + task_with_custom_attr = deepcopy(TASK_OS_DEPLOYMENT) + custom_attr = [{"SSH_CERT": "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"}] + task_with_custom_attr['custom_attributes'] = custom_attr + self.mock_ansible_instance.params = task_with_custom_attr + + hpe_icsp_os_deployment.main() + + personality_data = deepcopy(DEFAULT_SERVER) + personality_data['customAttributes'] = [ + {'values': [{'scope': 'server', 'value': "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"}], + 'key': 'SSH_CERT'}] + + self.mock_server_service.update_server.assert_called_once_with(personality_data) + + def test_get_server_by_serial_with_matching_result(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_SERVER])] + + server = hpe_icsp_os_deployment.get_server_by_serial(self.mock_connection, 'VCGYZ33007') + + expected = {'uri': '/rest/os-deployment-servers/123456'} + self.mock_connection.get.assert_called_once_with( + '/rest/index/resources?category=osdserver&query=\'osdServerSerialNumber:"VCGYZ33007"\'') + + self.assertEqual(server, expected) + + def test_get_server_by_serial_with_non_matching_result(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_SERVER])] + + server = hpe_icsp_os_deployment.get_server_by_serial(self.mock_connection, '000') + + self.mock_connection.get.assert_called_once_with( + '/rest/index/resources?category=osdserver&query=\'osdServerSerialNumber:"000"\'') + + self.assertIsNone(server) + + def test_get_build_plan_with_matching_result(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_BUILD_PLAN])] + + server = hpe_icsp_os_deployment.get_build_plan(self.mock_connection, 'RHEL 7.2 x64') + + expected = {'name': 'RHEL 7.2 x64', 'uri': '/rest/os-deployment-build-plans/222'} + self.mock_connection.get.assert_called_once_with( + '/rest/index/resources?filter="name=\'RHEL%207.2%20x64\'"&category=osdbuildplan') + + self.assertEqual(server, expected) + + def test_get_build_plan_with_non_matching_result(self): + self.mock_connection.get.side_effect = [self.get_as_rest_collection([DEFAULT_BUILD_PLAN])] + + server = hpe_icsp_os_deployment.get_build_plan(self.mock_connection, 'BuildPlan') + + self.mock_connection.get.assert_called_once_with( + '/rest/index/resources?filter="name=\'BuildPlan\'"&category=osdbuildplan') + + self.assertIsNone(server) + + +if __name__ == '__main__': + unittest.main() diff --git a/test/units/modules/cloud/hpe/test_hpe_icsp_server.py b/test/units/modules/cloud/hpe/test_hpe_icsp_server.py new file mode 100755 index 00000000000000..341bb2d751f410 --- /dev/null +++ b/test/units/modules/cloud/hpe/test_hpe_icsp_server.py @@ -0,0 +1,295 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# +# Copyright (2016-2017) Hewlett Packard Enterprise Development LP +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +import unittest +import mock +import yaml +import json + +from ansible.modules.cloud.hpe.hpe_icsp_server import (ICspServerModule, + main as hpe_icsp_server_main) + +from hpICsp.exceptions import HPICspInvalidResource + +MODULE_NAME = 'ansible.modules.cloud.hpe.hpe_icsp_server' + +SERVER_IP = "16.124.135.239" + +YAML_SERVER_PRESENT = """ + state: present + api_version: 300 + icsp_host: "16.124.133.245" + username: "Administrator" + password: "admin" + server_ipAddress: "16.124.135.239" + server_username: "Admin" + server_password: "serveradmin" + server_port: 443 +""" + +YAML_SERVER_ABSENT = """ + state: absent + api_version: 300 + icsp_host: "16.124.133.251" + username: "Administrator" + password: "admin" + server_ipAddress: "16.124.135.239" +""" + +YAML_NETWORK_CONFIGURED = """ + state: network_configured + api_version: 300 + icsp_host: "16.124.133.245" + username: "Administrator" + password: "admin" + server_ipAddress: "16.124.135.239" + server_username: "Admin" + server_password: "serveradmin" + server_port: 443 + server_personality_data: + network_config: + hostname: "test-web.io.fc.hpe.com" + domain: "demo.com" + interfaces: + - macAddress: "01:23:45:67:89:ab" + enabled: true + dhcpv4: false + ipv6Autoconfig: + dnsServers: + - "16.124.133.2" + staticNetworks: + - "16.124.133.39/255.255.255.0" + vlanid: -1 + ipv4gateway: "16.124.133.1" + ipv6gateway: + virtualInterfaces: +""" + +DEFAULT_SERVER = {"name": "SP-01", "uri": "/uri/239", "ilo": {"ipAddress": SERVER_IP}} +SERVER_ADDED = {"name": "SP-03", "uri": "/uri/188", "ilo": {"ipAddress": "16.124.135.188"}} + +SERVERS = { + "members": [ + DEFAULT_SERVER, + {"name": "SP-02", "uri": "/uri/233", "ilo": {"ipAddress": "16.124.135.233"}} + ] +} + +CONNECTION = {} +ICSP_JOBS = {} + +JOB_RESOURCE = {"uri": "/rest/os-deployment-jobs/123456"} + + +class IcspServerSpec(unittest.TestCase): + def setUp(self): + self.patcher_ansible_module = mock.patch(MODULE_NAME + '.AnsibleModule') + self.mock_ansible_module = self.patcher_ansible_module.start() + + self.mock_ansible_instance = mock.Mock() + self.mock_ansible_module.return_value = self.mock_ansible_instance + + self.patcher_icsp_service = mock.patch(MODULE_NAME + '.hpICsp') + self.mock_icsp = self.patcher_icsp_service.start() + + self.mock_connection = mock.Mock() + self.mock_connection.login.return_value = CONNECTION + self.mock_icsp.connection.return_value = self.mock_connection + + self.mock_server_service = mock.Mock() + self.mock_icsp.servers.return_value = self.mock_server_service + + def tearDown(self): + self.patcher_ansible_module.stop() + self.patcher_icsp_service.stop() + + def test_should_not_add_server_when_already_present(self): + self.mock_connection.get.return_value = SERVERS + self.mock_ansible_instance.params = yaml.load(YAML_SERVER_PRESENT) + + ICspServerModule().run() + + self.mock_ansible_instance.exit_json.assert_called_once_with( + changed=False, + msg=ICspServerModule.SERVER_ALREADY_PRESENT, + ansible_facts=dict(target_server=DEFAULT_SERVER) + ) + + def test_should_add_server(self): + self.mock_connection.get.side_effect = [{'members': []}, SERVERS] + self.mock_server_service.add_server.return_value = JOB_RESOURCE + self.mock_icsp.jobs.return_value = ICSP_JOBS + + self.mock_icsp.common = mock.Mock() + self.mock_icsp.common.monitor_execution.return_value = {} + + self.mock_ansible_instance.params = yaml.load(YAML_SERVER_PRESENT) + + hpe_icsp_server_main() + + ilo_body = {'ipAddress': "16.124.135.239", + 'username': "Admin", + 'password': "serveradmin", + 'port': 443} + self.mock_server_service.add_server.assert_called_once_with(ilo_body) + self.mock_icsp.common.monitor_execution.assert_called_once_with(JOB_RESOURCE, ICSP_JOBS) + + self.mock_ansible_instance.exit_json.assert_called_once_with( + changed=True, + msg="Server created: '/uri/239'", + ansible_facts=dict(target_server=DEFAULT_SERVER) + ) + + def test_expect_exception_not_caught_when_create_server_raise_exception(self): + self.mock_connection.get.side_effect = [{'members': []}, SERVERS] + self.mock_server_service.add_server.side_effect = Exception("message") + + self.mock_ansible_instance.params = yaml.load(YAML_SERVER_PRESENT) + + try: + ICspServerModule().run() + except Exception as e: + self.assertEqual("message", e.args[0]) + else: + self.fail("Expected Exception was not raised") + + def test_should_not_try_delete_server_when_it_is_already_absent(self): + self.mock_connection.get.return_value = {'members': []} + self.mock_server_service.delete_server.return_value = {} + self.mock_ansible_instance.params = yaml.load(YAML_SERVER_ABSENT) + + ICspServerModule().run() + + self.mock_server_service.delete_server.assert_not_called() + + self.mock_ansible_instance.exit_json.assert_called_once_with( + changed=False, + msg=ICspServerModule.SERVER_ALREADY_ABSENT + ) + + def test_should_delete_server(self): + self.mock_connection.get.return_value = SERVERS + + self.mock_server_service.delete_server.return_value = {} + + self.mock_ansible_instance.params = yaml.load(YAML_SERVER_ABSENT) + + ICspServerModule().run() + + self.mock_server_service.delete_server.assert_called_once_with("/uri/239") + + self.mock_ansible_instance.exit_json.assert_called_once_with( + changed=True, + msg="Server '/uri/239' removed successfully from ICsp." + ) + + def test_should_fail_with_all_exe_attr_when_HPICspException_raised_on_delete(self): + self.mock_connection.get.return_value = SERVERS + exeption_value = {"message": "Fake Message", "details": "Details", "errorCode": "INVALID_RESOURCE"} + self.mock_server_service.delete_server.side_effect = HPICspInvalidResource(exeption_value) + + self.mock_ansible_instance.params = yaml.load(YAML_SERVER_ABSENT) + + ICspServerModule().run() + + # Load called args and convert to dict to prevent str comparison with different reordering (Python 3.5) + fail_json_args_msg = self.mock_ansible_instance.fail_json.call_args[1]['msg'] + error_raised = json.loads(fail_json_args_msg) + self.assertEqual(error_raised, exeption_value) + + def test_should_fail_with_args_joined_when_common_exception_raised_on_delete(self): + self.mock_connection.get.return_value = SERVERS + self.mock_server_service.delete_server.side_effect = Exception("Fake Message", "INVALID_RESOURCE") + + self.mock_ansible_instance.params = yaml.load(YAML_SERVER_ABSENT) + + ICspServerModule().run() + + self.mock_ansible_instance.fail_json.assert_called_once_with(msg='Fake Message; INVALID_RESOURCE') + + def test_should_configure_network(self): + self.mock_connection.get.side_effect = [SERVERS, SERVERS] + self.mock_connection.post.return_value = JOB_RESOURCE + self.mock_server_service.get_server.return_value = DEFAULT_SERVER + + self.mock_ansible_instance.params = yaml.load(YAML_NETWORK_CONFIGURED) + + ICspServerModule().run() + + network_config_state = yaml.load(YAML_NETWORK_CONFIGURED) + + network_config = { + "serverData": [ + {"serverUri": DEFAULT_SERVER['uri'], "personalityData": network_config_state['server_personality_data'], + "skipReboot": True}], + "failMode": None, + "osbpUris": [] + } + + uri = '/rest/os-deployment-jobs/?writeOnly=true' + + self.mock_connection.post.assert_called_once_with(uri, network_config) + + self.mock_ansible_instance.exit_json.assert_called_once_with( + changed=True, + msg=ICspServerModule.CUSTOM_ATTR_NETWORK_UPDATED, + ansible_facts=dict(target_server=DEFAULT_SERVER) + ) + + def test_should_fail_when_try_configure_network_without_inform_personality_data(self): + self.mock_connection.get.return_value = SERVERS + self.mock_server_service.get_server.return_value = DEFAULT_SERVER + + params_config_network = yaml.load(YAML_NETWORK_CONFIGURED) + params_config_network['server_personality_data'] = {} + + self.mock_ansible_instance.params = params_config_network + + ICspServerModule().run() + + self.mock_ansible_instance.fail_json.assert_called_once_with( + msg=ICspServerModule.SERVER_PERSONALITY_DATA_REQUIRED) + + def test_should_fail_when_try_configure_network_for_not_found_server(self): + self.mock_connection.get.return_value = {'members': []} + + self.mock_ansible_instance.params = yaml.load(YAML_NETWORK_CONFIGURED) + + ICspServerModule().run() + + self.mock_ansible_instance.exit_json.assert_called_once_with(changed=False, + msg=ICspServerModule.SERVER_NOT_FOUND) + + def test_expect_exception_not_caught_when_configure_network_raise_exception(self): + self.mock_connection.get.return_value = SERVERS + self.mock_connection.post.side_effect = Exception("message") + + self.mock_ansible_instance.params = yaml.load(YAML_NETWORK_CONFIGURED) + + try: + hpe_icsp_server_main() + except Exception as e: + self.assertEqual("message", e.args[0]) + else: + self.fail("Expected Exception was not raised") + + +if __name__ == '__main__': + unittest.main()