diff --git a/os_xenapi/cmd/bootstrap.py b/os_xenapi/cmd/bootstrap.py new file mode 100644 index 0000000..04540d6 --- /dev/null +++ b/os_xenapi/cmd/bootstrap.py @@ -0,0 +1,106 @@ +# Copyright 2017 Citrix Systems +# +# 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. + +"""Command for XenAPI bootstrap. + +It contains any needed work to bootstrap a XenServer node, so that it's in +a good state to proceed for further OpenStack deployment.""" + +import getopt +import json +import sys + +from os_xenapi.utils.himn import config_himn +from os_xenapi.utils.iptables import config_iptables +from os_xenapi.utils.sshclient import SSHClient +from os_xenapi.utils.xapi_plugin import install_plugins_to_dom0 +from os_xenapi.utils.xenapi_facts import get_xenapi_facts + +USAGE_MSG = "Run the following command to bootstrap the XenAPI compute node:\n" +USAGE_MSG += sys.argv[0] +USAGE_MSG += " [-i|--himn-ip] " +USAGE_MSG += " [-f|--xenapi-facts-file] " +USAGE_MSG += " [-u|--user-name] " +USAGE_MSG += " [-p|--passwd] \n\n" + +DEF_XENAPI_FACTS_FILE = '/etc/xenapi_facts.json' + + +def exit_with_usage(): + sys.stderr.write(USAGE_MSG) + sys.exit(1) + + +def get_and_store_facts(dom0_client, file_path): + facts = get_xenapi_facts(dom0_client) + with open(file_path, 'w') as f: + f.write(json.dumps(facts, indent=4, sort_keys=True)) + + +def _parse_args(argv): + VALID_OPS_SHORT_STR = "i:f:p:u:" + VALID_OPS_LONG_LST = ["himn-ip", "xenapi-facts-file", + "passwd", "user-name"] + MANDATORY_OPT_LST = ["himn-ip", "passwd", "user-name"] + opt_values = {} + + if len(argv) < 2: + return exit_with_usage() + argv = argv[1:] + + try: + opts, args = getopt.getopt(argv, VALID_OPS_SHORT_STR, + VALID_OPS_LONG_LST) + except getopt.GetoptError: + return exit_with_usage() + + # Get the values from input parameters. + for opt, arg in opts: + if opt in ("-i", "--himn-ip"): + opt_values['himn-ip'] = arg + elif opt in ("-f", "--xenapi-facts-file"): + opt_values['xenapi-facts-file'] = arg + elif opt in ("-p", "--passwd"): + opt_values['passwd'] = arg + elif opt in ("-u", "--user-name"): + opt_values['user-name'] = arg + + # Ensure mandatory opts are all provided. + for opt in MANDATORY_OPT_LST: + if opt not in opt_values: + return exit_with_usage() + + return opt_values + + +def main(): + opt_values = _parse_args(sys.argv) + + himn_ip = opt_values['himn-ip'] + user_name = opt_values['user-name'] + passwd = opt_values['passwd'] + # Use DEF_XENAPI_FACTS_FILE if none provided via commandline. + facts_file = opt_values.get('xenapi-facts-file', DEF_XENAPI_FACTS_FILE) + dom0_client = SSHClient(himn_ip, user_name, passwd) + + # Invoke functions to do needed boostrap tasks. + config_himn(himn_ip) + config_iptables(dom0_client) + install_plugins_to_dom0(dom0_client) + + # Gather XenAPI relative facts and save them into file. + get_and_store_facts(dom0_client, facts_file) + +if __name__ == "__main__": + sys.exit(main()) diff --git a/os_xenapi/tests/cmd/test_bootstrap.py b/os_xenapi/tests/cmd/test_bootstrap.py new file mode 100644 index 0000000..596b3e5 --- /dev/null +++ b/os_xenapi/tests/cmd/test_bootstrap.py @@ -0,0 +1,93 @@ +# 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. + +import mock + +from os_xenapi.cmd import bootstrap +from os_xenapi.tests import base + + +class GetXenapiFactsTestCase(base.TestCase): + def test_parse_args(self): + argv = ['bootstrap', '-i', '169.254.0.1', '-u', 'root', '-p', 'passwd'] + + return_opts = bootstrap._parse_args(argv) + + expect_opts = {'himn-ip': '169.254.0.1', + 'passwd': 'passwd', + 'user-name': 'root'} + self.assertEqual(expect_opts, return_opts) + + def test_parse_args_with_filepath(self): + argv = ['bootstrap', '-i', '169.254.0.1', '-u', 'root', '-p', 'passwd', + '-f', '/path/to/file'] + + return_opts = bootstrap._parse_args(argv) + + expect_opts = {'himn-ip': '169.254.0.1', + 'passwd': 'passwd', + 'user-name': 'root', + 'xenapi-facts-file': '/path/to/file'} + self.assertEqual(expect_opts, return_opts) + + @mock.patch.object(bootstrap, 'exit_with_usage') + def test_parse_args_no_valid_option(self, mock_usage): + # Verify if it will exit with prompting usage if no + # valid options passed except the command name. + argv = ['bootstrap'] + + bootstrap._parse_args(argv) + + mock_usage.assert_called_with() + + @mock.patch.object(bootstrap, 'exit_with_usage') + def test_parse_args_invalid_opts(self, mock_usage): + # Verify if it will exit with prompting usage if pass in + # wrong opts. + argv = ['bootstrap', '-v', 'invalid_opt'] + + bootstrap._parse_args(argv) + + mock_usage.assert_called_with() + + @mock.patch.object(bootstrap, 'exit_with_usage') + def test_parse_args_lack_opts(self, mock_usage): + # Verify if it will exit with prompting usage if not + # pass in all required opts. + argv = ['bootstrap', '-i', '169.254.0.1'] + + bootstrap._parse_args(argv) + + mock_usage.assert_called_with() + + @mock.patch.object(bootstrap, '_parse_args') + @mock.patch.object(bootstrap, 'SSHClient') + @mock.patch.object(bootstrap, 'config_himn') + @mock.patch.object(bootstrap, 'config_iptables') + @mock.patch.object(bootstrap, 'install_plugins_to_dom0') + @mock.patch.object(bootstrap, 'get_and_store_facts') + def test_bootstrap(self, mock_facts, mock_plugin, mock_iptables, + mock_himn, mock_client, mock_parse): + fake_opts = {'himn-ip': '169.254.0.1', + 'passwd': 'passwd', + 'user-name': 'root'} + mock_parse.return_value = fake_opts + mock_client.return_value = mock.sentinel.sshclient + + bootstrap.main() + + mock_client.assert_called_with('169.254.0.1', 'root', 'passwd') + mock_himn.assert_called_with('169.254.0.1') + mock_iptables.assert_called_with(mock.sentinel.sshclient) + mock_plugin.assert_called_with(mock.sentinel.sshclient) + mock_facts.assert_called_with(mock.sentinel.sshclient, + bootstrap.DEF_XENAPI_FACTS_FILE) diff --git a/setup.cfg b/setup.cfg index 3030650..95ec9a7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,10 @@ classifier = packages = os_xenapi +[entry_points] +console_scripts = + xenapi_bootstrap = os_xenapi.cmd.bootstrap:main + [build_sphinx] source-dir = doc/source build-dir = doc/build