From 7f7cfccc1586326647b67e3a2385cd6c63f95643 Mon Sep 17 00:00:00 2001 From: Osvaldo Demo Date: Fri, 22 May 2015 12:37:58 +1200 Subject: [PATCH] Implementation for using existing xml credential store files made with credstore_admin.pl (Solving opened issue #20) --- pyvmomi_tools/extensions/credstore.py | 204 ++++++++++++++++++ .../virtual_machine_power_cycle_credstore.py | 176 +++++++++++++++ tests/test_credstore.py | 113 ++++++++++ 3 files changed, 493 insertions(+) create mode 100644 pyvmomi_tools/extensions/credstore.py create mode 100644 samples/virtual_machine_power_cycle_credstore.py create mode 100644 tests/test_credstore.py diff --git a/pyvmomi_tools/extensions/credstore.py b/pyvmomi_tools/extensions/credstore.py new file mode 100644 index 0000000..c2ba707 --- /dev/null +++ b/pyvmomi_tools/extensions/credstore.py @@ -0,0 +1,204 @@ +# Copyright (c) 2014 VMware, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +''' +Minimal functionality to read and use passwords from vSphere Credential Store XML file +''' + +from __future__ import print_function + +__author__ = 'Osvaldo Demo' + +import xml.etree.ElementTree as ET +from sys import platform as _platform +import os +import os.path + + +class PasswordEntry(object): + """ + Abstraction object that translates from obfuscated password to usable password text + """ + + def __init__ (self, host=None, username=None, password=None): + self.__host = host + self.__username = username + self.__password = password + + def __str__ (self): + return '{ Host: ' + self.__host + ' User: ' + self.__username + ' Pwd: ' + self.__password + ' }' + + def __unicode__ (self): + return self.__str__() + + def __repr__(self): + return self.__str__() + + def __hash__(self): + return hash(self.__host) + hash(self.__username) + hash(self.__password) + + def __eq__(self, other): + if isinstance(other, self.__class__): + return hash(self) == hash(other) + else: + return False + + def _compute_hash (self, text): + """ + Generates a hash based on the following formula: + + hash = s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] + + :param text: String + :return: Computed hash value + :rtype: int + """ + + boundary = 0x7FFFFFFF + negative = 0x80000000 + hash_value = 0 + for my_char in text: + hash_value = hash_value * 31 + ord(my_char) + if (hash_value & negative): + hash_value |= ~boundary + else: + hash_value &= boundary + + return hash_value + + def _deobfuscate (self): + """ + Convert the obfuscated string to the actual password in clear text + + Functionality taken from the perl module VICredStore.pm since the goal was to emulate its behaviour. + + """ + + hashmod = 256 + password = self.__password.decode('base64') + hash_value = self._compute_hash(self.__host + self.__username) % hashmod + crypt = chr(hash_value & 0xFF) * len(password) + password_final = [] + for n in range(0, len(password)): + password_final.append(ord(password[n]) ^ ord(crypt[n])) + decrypted_pwd = '' + for ci in password_final: + if ci == 0: + break + decrypted_pwd += chr(ci) + + return decrypted_pwd + + def getPwd (self): + return self._deobfuscate() + + def getUser (self): + return self.__username + + def getHost (self): + return self.__host + + +class HostNotFoundException(Exception): + """ + Exception raised when the host/server was not found in the credentials file. + """ + pass + + +class NoCredentialsFileFound(Exception): + """ + Exception raised when the credentials xml file was not found. + """ + pass + + +class VICredStore(object): + """ + Helper class that mimicks VICredStore perl module. + + Functionality implemented to decode the existing credentials file only. + """ + + __hostdata = {} + FILE_PATH_UNIX = '/.vmware/credstore/vicredentials.xml' + FILE_PATH_WIN = '/VMware/credstore/vicredentials.xml' + + def __init__ (self, path=None): + if path is None: + try: + if os.environ['VI_CREDSTORE'] is not None: + self.__path = os.environ['VI_CREDSTORE'] + except KeyError: + if _platform == "linux" or _platform == "linux2": + self.__path = os.environ['HOME'] + self.FILE_PATH_UNIX + elif _platform == "win32": + self.__path = os.environ['APPDATA'] + self.FILE_PATH_WIN + else: + raise Exception('Unsupported platform! (' + _platform + ')') + else: + self.__path = path + + if os.path.exists(self.__path): + self.__tree = ET.parse(self.__path) + self.__root = self.__tree.getroot() + self.__hostdata = self.__populate_data() + else: + self.__root = None + self.__tree = None + raise NoCredentialsFileFound('Credential filename [' + self.__path + '] doesn\'t exist!') + + def get_userpwd (self, hostname): + try: + entry = self.__hostdata[hostname] + except KeyError: + raise HostNotFoundException("Host " + hostname + " does not exist in the credential store!") + + return (entry.getUser(), entry.getPwd()) + + def _get_pwd_entry_list (self): + tmp_list = [] + for entry in self.__root: + if entry.tag == "passwordEntry": + tmp_list.append(entry) + + pwdentries = [] + for entry in tmp_list: + host = None + user = None + pwd = None + for child in entry: + if child.tag == "host": + host = child.text + if child.tag == "username": + user = child.text + if child.tag == "password": + pwd = child.text + + if host is not None and user is not None and pwd is not None: + pwdentries.append(PasswordEntry(host, user, pwd)) + + return pwdentries + + def list_entries (self): + for entry in sorted(self.__hostdata.keys()): + print(entry) + + def __populate_data (self): + pwd_list = self._get_pwd_entry_list() + new_hostdata = {} + for entry in pwd_list: + new_hostdata[entry.getHost()] = entry + + return new_hostdata diff --git a/samples/virtual_machine_power_cycle_credstore.py b/samples/virtual_machine_power_cycle_credstore.py new file mode 100644 index 0000000..f51b6cb --- /dev/null +++ b/samples/virtual_machine_power_cycle_credstore.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +# Copyright (c) 2014 VMware, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import print_function + +""" +A Python script for power cycling a virtual machine. Demonstrates the use +of tasks in an asynchronous way, how to answer virtual machine +questions in the middle of power operations using a credentials xml file. + +In order to use it you need to create your credentials file via: + + # credstore_admin.pl add -s -u -p + +get_args method has been modified to illustrate the functionality. +This could be integrated to the cli package in the args file. + +""" + +import atexit +import argparse +from six import PY2 +import sys +import textwrap + +from pyvmomi_tools import cli +from pyVim import connect +from pyVmomi import vim +from pyvmomi_tools.extensions.credstore import VICredStore, NoCredentialsFileFound, HostNotFoundException + +if PY2: + input = raw_input + +def get_args(): + """ + Supports the command-line arguments and/or credentials xml file. + + """ + parser = argparse.ArgumentParser(description='Process args for retrieving all the Virtual Machines') + parser.add_argument('-s', '--host', + required=True, + action='store', + help='Remote host to connect to') + parser.add_argument('-o', '--port', + type=int, + default=443, + action='store', + help='Port to connect on') + parser.add_argument('-u', '--user', + action='store', + help='User name to use when connecting to host') + parser.add_argument('-p', '--password', + action='store', + help='Password to use when connecting to host') + parser.add_argument('-n', '--name', + required=True, + action='store', + help='Name of the virtual_machine to look for.') + + args = parser.parse_args() + + try: + store = VICredStore() + except NoCredentialsFileFound: + print("ERROR: No credentials store file found. You need to enter credentials via command-line arguments!\n") + sys.exit(1) + + try: + (args.user, args.password) = store.get_userpwd(args.host) + except HostNotFoundException: + print("ERROR: Host [" + args.host + "] was not found on credentials file. You need to enter credentials via command-line!\n") + parser.print_usage() + sys.exit(1) + + return args + +args = get_args() + +# form a connection... +si = connect.SmartConnect(host=args.host, user=args.user, pwd=args.password, + port=args.port) + +# doing this means you don't need to remember to disconnect your script/objects +atexit.register(connect.Disconnect, si) + +# search the whole inventory tree recursively... a brutish but effective tactic +vm = si.content.rootFolder.find_by_name(args.name) +if not isinstance(vm, vim.VirtualMachine): + print("could not find a virtual machine with the name {0}", args.name) + sys.exit(-1) + +print("Found VirtualMachine: {0} Name: {1}", vm, vm.name) + +if vm.runtime.powerState == vim.VirtualMachinePowerState.poweredOn: + # using a dynamic class extension for power_off + # this is a blocking method call and the script will pause + # while the machine powers off. + print("powering off...") + vm.power_off() + print("power is off.") + + +def answer_question(vm): + print("\n") + choices = vm.runtime.question.choice.choiceInfo + default_option = None + if vm.runtime.question.choice.defaultIndex is not None: + ii = vm.runtime.question.choice.defaultIndex + default_option = choices[ii] + choice = None + while choice not in [o.key for o in choices]: + print("VM power on is paused by this question:\n\n") + print("\n".join(textwrap.wrap(vm.runtime.question.text, 60))) + for option in choices: + print("\t {0}: {1} ", option.key, option.label) + if default_option is not None: + print("default ({0}): {1}\n", default_option.label, + default_option.key) + choice = input("\nchoice number: ").strip() + print("...") + return choice + + +# Sometimes we don't want a task to block execution completely +# we may want to execute or handle concurrent events. In that case we can +# poll our task repeatedly and also check for any run-time issues. This +# code deals with a common problem, what to do if a VM question pops up +# and how do you handle it in the API? +print("powering on VM {0}", vm.name) +if vm.runtime.powerState != vim.VirtualMachinePowerState.poweredOn: + + # now we get to work... calling the vSphere API generates a task... + task = vm.PowerOn() + + # We track the question ID & answer so we don't end up answering the same + # questions repeatedly. + answers = {} + + def handle_question(current_task, virtual_machine): + # we'll check for a question, if we find one, handle it, + # Note: question is an optional attribute and this is how pyVmomi + # handles optional attributes. They are marked as None. + if virtual_machine.runtime.question is not None: + question_id = virtual_machine.runtime.question.id + if question_id not in answers.keys(): + answer = answer_question(virtual_machine) + answers[question_id] = answer + virtual_machine.AnswerVM(question_id, answer) + + # create a spinning cursor so people don't kill the script... + cli.cursor.spinner(task.info.state) + + task.poll(vm, periodic=handle_question) + + if task.info.state == vim.TaskInfo.State.error: + # some vSphere errors only come with their class and no other message + print("error type: {0}", task.info.error.__class__.__name__) + print("found cause: {0}", task.info.error.faultCause) + for fault_msg in task.info.error.faultMessage: + print(fault_msg.key) + print(fault_msg.message) + sys.exit(-1) + +print(".") +sys.exit(0) diff --git a/tests/test_credstore.py b/tests/test_credstore.py new file mode 100644 index 0000000..d2fc5fb --- /dev/null +++ b/tests/test_credstore.py @@ -0,0 +1,113 @@ +# Copyright (c) 2014 VMware, Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +''' +Minimal functionality to read and use passwords from vSphere Credential Store XML file +''' + +from __future__ import print_function +import os +import shutil +import unittest +import sys +from pyvmomi_tools.extensions.credstore import VICredStore, NoCredentialsFileFound, HostNotFoundException, PasswordEntry +from sys import platform as _platform +from cStringIO import StringIO + +__author__ = 'Osvaldo Demo' + + +class VICredStoreTests(unittest.TestCase): + + def _create_credentialsfile(self, file): + target = open(file, 'w') + target.write('') + target.write("\n") + target.write(' ') + target.write("\n") + target.write(' 1.0') + target.write("\n") + target.write(' ') + target.write("\n") + target.write(' mytesthost') + target.write("\n") + target.write(' testuser') + target.write("\n") + target.write(' NyYwNzMiMDA0LDEnQwY6EwoWFHsINgUwdCV1cg1wDyUtJBssG3cicRE7MQcVKxp1FhsOHBMrdSASNwoJCXM2cjUaOy0JJXsIFXN2EgAsKzUmeiU6EzIvcisrBAEIdg87IQs7JRI3DRwQMRsAMwIGJw8CAXQuDjslJRERKnEmB0M=') + target.write("\n") + target.write(' ') + target.write("\n") + target.write('') + target.write("\n") + + def setUp(self): + self.test_path = "mycredentials.xml" + self._create_credentialsfile(self.test_path) + self.path = None + + if _platform == "linux" or _platform == "linux2": + self.path = os.environ['HOME'] + VICredStore.FILE_PATH_UNIX + elif _platform == "win32": + self.path = os.environ['APPDATA'] + VICredStore.FILE_PATH_WIN + + if self.path is not None: + os.path.exists(self.path) + shutil.copy(self.path,self.path+'.bak') + shutil.copy(self.test_path,self.path) + + def tearDown(self): + os.remove('mycredentials.xml') + if self.path is not None: + os.path.exists(self.path+'.bak') + shutil.copy(self.path+'.bak',self.path) + os.remove(self.path+'.bak') + + def test_get_userpwd(self): + os.environ['VI_CREDSTORE'] = self.test_path + store = VICredStore(os.environ['VI_CREDSTORE']) + self.assertEqual(store.get_userpwd('mytesthost'),('testuser','testpassword')) + + def test_get_userpwd_2(self): + store = VICredStore() + self.assertEqual(store.get_userpwd('mytesthost'),('testuser','testpassword')) + + def test_get_userpwd_3(self): + os.environ.pop('VI_CREDSTORE',None) + if self.path is not None: + store = VICredStore() + self.assertEqual(store.get_userpwd('mytesthost'),('testuser','testpassword')) + + def test_VICredStore_NoCredentialsFileFound(self): + self.assertRaises(NoCredentialsFileFound,VICredStore,'anyfile.xml') + + def test_get_userpwd_HostNotFoundException(self): + os.environ['VI_CREDSTORE'] = self.test_path + store = VICredStore(os.environ['VI_CREDSTORE']) + self.assertRaises(HostNotFoundException,store.get_userpwd,'notexistanthost') + + def test_get_pwd_entry_list(self): + os.environ['VI_CREDSTORE'] = self.test_path + store = VICredStore(os.environ['VI_CREDSTORE']) + pwdentry = PasswordEntry('mytesthost','testuser','NyYwNzMiMDA0LDEnQwY6EwoWFHsINgUwdCV1cg1wDyUtJBssG3cicRE7MQcVKxp1FhsOHBMrdSASNwoJCXM2cjUaOy0JJXsIFXN2EgAsKzUmeiU6EzIvcisrBAEIdg87IQs7JRI3DRwQMRsAMwIGJw8CAXQuDjslJRERKnEmB0M=') + pwdlist = store._get_pwd_entry_list() + self.assertEqual(len(pwdlist),1) + self.assertEqual(pwdentry,pwdlist[0]) + + def test_list_entries(self): + self.held, sys.stdout = sys.stdout, StringIO() + os.environ['VI_CREDSTORE'] = self.test_path + store = VICredStore(os.environ['VI_CREDSTORE']) + store.list_entries() + self.assertEqual(sys.stdout.getvalue(),'mytesthost\n') + sys.stdout = self.held